// Copyright 2025 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/actor/actor_keyed_service.h"

#include <optional>
#include <utility>

#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/actor/actor_critical_action_logger.h"
#include "chrome/browser/actor/actor_keyed_service_factory.h"
#include "chrome/browser/actor/actor_metrics.h"
#include "chrome/browser/actor/actor_proto_conversion.h"
#include "chrome/browser/actor/actor_tab_data.h"
#include "chrome/browser/actor/actor_task.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/application_status_listener.h"
#include "chrome/browser/actor/android/actor_keyed_service_android.h"
#endif
#include "chrome/browser/actor/actor_task_metadata.h"
#include "chrome/browser/actor/actor_util.h"
#include "chrome/browser/actor/enterprise_policy_checker.h"
#include "chrome/browser/actor/execution_engine.h"
#include "chrome/browser/actor/tab_observation_strategy.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/ui/actor_ui_state_manager.h"
#include "chrome/browser/actor/ui/event_dispatcher.h"
#include "chrome/browser/page_content_annotations/multi_source_page_context_fetcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/tab_list/tab_removed_reason.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/webui_url_constants.h"
#include "components/actor/core/actor_features.h"
#include "components/actor/core/aggregated_journal.h"
#include "components/actor/core/journal_details_builder.h"
#include "components/actor/core/task_id.h"
#include "components/actor/public/mojom/actor_types.mojom.h"
#include "components/optimization_guide/proto/features/common_quality_data.pb.h"
#include "components/origin_gating/core/actor_container_config_slot.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_switches.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "ui/base/window_open_disposition.h"

namespace {

static constexpr int kNoTabFound = -1;

void RunLater(base::OnceClosure task) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
                                                              std::move(task));
}

void OnCreateActorTabComplete(
    actor::ActorTask& task,
    actor::ActorKeyedService::CreateActorTabCallback callback,
    actor::AggregatedJournal& journal,
    tabs::TabInterface* tab) {
  if (base::FeatureList::IsEnabled(actor::kActorBindCreatedTabToTask) && tab) {
    task.AddTab(
        tab->GetHandle(),
        /*stop_task_on_detach=*/true,
        base::BindOnce(
            [](actor::ActorKeyedService::CreateActorTabCallback callback,
               tabs::TabHandle handle, actor::TaskId task_id,
               base::WeakPtr<actor::AggregatedJournal> journal,
               actor::mojom::ActionResultPtr result) {
              if (journal) {
                journal->Log(
                    GURL(), task_id, "OnCreateActorTabComplete",
                    actor::JournalDetailsBuilder()
                        .Add("AddTab result", actor::ToDebugString(*result))
                        .Build());
              }
              std::move(callback).Run(handle.Get());
            },
            std::move(callback), tab->GetHandle(), task.id(),
            journal.GetWeakPtr()));
  } else {
    std::move(callback).Run(tab);
  }
}

}  // namespace

namespace actor {

namespace {
BASE_FEATURE(kGlicActorFixPageObservationCrash,
             base::FEATURE_ENABLED_BY_DEFAULT);
}

std::optional<page_content_annotations::PaintPreviewOptions>
CreateOptionalPaintPreviewOptions() {
  if (!base::FeatureList::IsEnabled(kGlicTabScreenshotPaintPreviewBackend)) {
    return std::nullopt;
  }
  page_content_annotations::PaintPreviewOptions paint_preview_options;
  paint_preview_options.max_per_capture_bytes =
      kScreenshotMaxPerCaptureBytes.Get();
  paint_preview_options.iframe_redaction_scope =
      kScreenshotIframeRedaction.Get();
  return paint_preview_options;
}

using ui::ActorUiStateManagerInterface;

ActorKeyedService::ActorKeyedService(Profile* profile) : profile_(profile) {
  actor_ui_state_manager_ = std::make_unique<ui::ActorUiStateManager>(*this);
  profile_observation_.Observe(profile_);
  actor::InitActionBlocklist(profile_);
}

void ActorKeyedService::OnProfileInitializationComplete(Profile* profile) {
  // `download_notifier_` is set up after profile initialization because
  // `GetDownloadManager()` relies on other download services to be fully
  // initialized which may not be true until the profile is initialized.
  download_notifier_ = std::make_unique<download::AllDownloadItemNotifier>(
      profile_->GetDownloadManager(), this);
}

ActorKeyedService::~ActorKeyedService() = default;

void ActorKeyedService::Shutdown() {
  // Ensure all tasks are stopped here so we don't cause them to stop in the
  // dtor.
  GetJournal().Log(GURL(), TaskId(), "ActorKeyedService::Shutdown", {});
  for (auto [task_id, _] : GetActiveTasks()) {
    StopTask(task_id, ActorTask::StoppedReason::kShutdown);
  }

  // Ensure tasks get deleted synchronously to avoid dangling refs.
  CHECK(active_tasks_.empty());
  pending_delete_tasks_.clear();
}

// static
ActorKeyedService* ActorKeyedService::Get(content::BrowserContext* context) {
  return ActorKeyedServiceFactory::GetActorKeyedService(context);
}

void ActorKeyedService::SetActorUiStateManagerForTesting(
    std::unique_ptr<ui::ActorUiStateManagerInterface> ausm) {
  CHECK(ausm);
  actor_ui_state_manager_ = std::move(ausm);
}

const ActorTask* ActorKeyedService::GetActingActorTaskForWebContents(
    content::WebContents* web_contents) {
  if (auto* tab_interface =
          tabs::TabInterface::MaybeGetFromContents(web_contents)) {
    // There should only be one active task per tab.
    for (const auto& [task_id, actor_task] : GetActiveTasks()) {
      if (actor_task->IsActingOnTab(tab_interface->GetHandle())) {
        return actor_task;
      }
    }
  }

  return nullptr;
}

void ActorKeyedService::CreateActorTab(TaskId task_id,
                                       bool open_in_background,
                                       tabs::TabHandle initiator_tab_handle,
                                       SessionID initiator_window_id,
                                       CreateActorTabCallback callback) {
  GetJournal().Log(
      GURL(), task_id, "CreateActorTab",
      JournalDetailsBuilder()
          .Add("task_id", task_id)
          .Add("open_in_background", open_in_background)
          .Add("initiator_tab_id", initiator_tab_handle.raw_value())
          .Add("initiator_window_id", initiator_window_id.id())
          .Build());
  ActorTask* task = GetTask(task_id);
  if (!task) {
    GetJournal().Log(GURL(), task_id, "CreateActorTab",
                     JournalDetailsBuilder().AddError("Invalid Task").Build());
    std::move(callback).Run(nullptr);
    return;
  }

  BrowserWindowInterface* window_for_new_tab = nullptr;
  tabs::TabInterface* initiator_tab = initiator_tab_handle.Get();

  // Special case: if the initiator tab is the NTP, no need to create a new
  // tab, reuse it.
  // TODO(crbug.com/537432406): Check for about:blank URL in addition to NTP to
  // reuse empty tabs.
  if (initiator_tab && search::IsNTPURL(initiator_tab->GetContents()
                                            ->GetPrimaryMainFrame()
                                            ->GetLastCommittedURL())) {
    GetJournal().Log(
        GURL(), task_id, "CreateActorTab",
        JournalDetailsBuilder().Add("Return", "Initiator is NTP").Build());

    if (!open_in_background) {
      BrowserWindowInterface* window =
          initiator_tab->GetBrowserWindowInterface();
      // TODO(b/482430429): figure out a way to activate a tab when there's no
      // BWI on Android.
      if (window) {
        TabListInterface* tab_list = TabListInterface::From(window);
        if (tab_list &&
            tab_list->GetIndexOfTab(initiator_tab_handle) != kNoTabFound) {
          tab_list->ActivateTab(initiator_tab_handle);
        }
      }
    }

    OnCreateActorTabComplete(*task, std::move(callback), journal_,
                             initiator_tab);
    return;
  }

#if BUILDFLAG(IS_ANDROID)
  if (!base::android::ApplicationStatusListener::HasVisibleActivities()) {
    CreateBackgroundTabForTask(
        profile_, task_id,
        base::BindOnce(
            [](base::WeakPtr<ActorKeyedService> service, TaskId task_id,
               CreateActorTabCallback callback, tabs::TabInterface* tab) {
              if (!service) {
                if (tab) {
                  tab->Close();
                }
                std::move(callback).Run(nullptr);
                return;
              }
              if (!tab) {
                service->GetJournal().Log(
                    GURL(), task_id, "CreateBackgroundTabForTask",
                    JournalDetailsBuilder()
                        .AddError("Background tab creation failed")
                        .Build());
              } else {
                service->GetJournal().Log(
                    GURL(), task_id, "CreateBackgroundTabForTask",
                    JournalDetailsBuilder().Add("Success", true).Build());
              }
              ActorTask* task = service->GetTask(task_id);
              if (!task) {
                if (tab) {
                  tab->Close();
                }
                std::move(callback).Run(nullptr);
                return;
              }
              OnCreateActorTabComplete(*task, std::move(callback),
                                       service->GetJournal(), tab);
            },
            weak_ptr_factory_.GetWeakPtr(), task_id, std::move(callback)));
    return;
  }
#endif

  // If the initiating tab is still live, create the new tab in the same window.
  if (initiator_tab) {
    if (initiator_tab->IsInNormalWindow()) {
      window_for_new_tab = initiator_tab->GetBrowserWindowInterface();
      if (window_for_new_tab) {
        GetJournal().Log(GURL(), task_id, "CreateActorTab",
                         JournalDetailsBuilder()
                             .Add("Using initiator_tab's window",
                                  window_for_new_tab->GetSessionID().id())
                             .Build());
      }
    }
  } else {
    // TODO(b/482430429): Figure out how to proceed from just a window ID on
    // Android.
#if !BUILDFLAG(IS_ANDROID)
    // If the tab was closed, open it in the window it was in (at the time of
    // task initiation).
    window_for_new_tab =
        BrowserWindowInterface::FromSessionID(initiator_window_id);
    GetJournal().Log(
        GURL(), task_id, "CreateActorTab",
        JournalDetailsBuilder()
            .Add("Using initiator_window", initiator_window_id.id())
            .Build());
#endif
  }

  NavigateParams params(profile_.get(), GURL(url::kAboutBlankURL),
                        ::ui::PAGE_TRANSITION_AUTO_TOPLEVEL);

  if (window_for_new_tab) {
    params.disposition = open_in_background
                             ? WindowOpenDisposition::NEW_BACKGROUND_TAB
                             : WindowOpenDisposition::NEW_FOREGROUND_TAB;
    params.browser = window_for_new_tab;
    params.window_action = NavigateParams::WindowAction::kNoAction;

    if (initiator_tab) {
      TabListInterface* window_tab_list =
          TabListInterface::From(window_for_new_tab);
      if (window_tab_list) {
        int initiator_index =
            window_tab_list->GetIndexOfTab(initiator_tab_handle);
        if (initiator_index != kNoTabFound) {
          params.tabstrip_index = initiator_index + 1;
        }
      }
    }
#if BUILDFLAG(IS_ANDROID)
    if (open_in_background) {
      // Workaround for b/489440503. On Android we ignore
      // WindowOpenDisposition::NEW_BACKGROUND_TAB when a tabstrip_index is set,
      // so revert the index to its default value.
      params.tabstrip_index = -1;
    }
#endif
  } else {
    GetJournal().Log(
        GURL(), task_id, "CreateActorTab",
        JournalDetailsBuilder().Add("Creating New Window", "").Build());
    // If window_for_new_tab is still null (e.g. the initiating window was
    // closed) the tab will be created in a new window.
    // TODO(b/454046200): Reconsider what should happen in this case.
    params.disposition = WindowOpenDisposition::NEW_WINDOW;
    params.window_action = NavigateParams::WindowAction::kShowWindow;
  }

  // TODO(b/490182433) Use async version of Navigate() when b/490180494 is
  // fixed. This is needed to support opening new windows on Android.
  base::WeakPtr<content::NavigationHandle> handle = Navigate(&params);
  if (!handle) {
    GetJournal().Log(
        GURL(), task_id, "CreateActorTab",
        JournalDetailsBuilder().AddError("Failed creating navigation").Build());
    OnCreateActorTabComplete(*task, std::move(callback), journal_, nullptr);
    return;
  }

  content::WebContents* contents = handle->GetWebContents();
  if (!contents) {
    GetJournal().Log(GURL(), task_id, "CreateActorTab",
                     JournalDetailsBuilder()
                         .AddError("Navigation missing WebContents")
                         .Build());
    OnCreateActorTabComplete(*task, std::move(callback), journal_, nullptr);
    return;
  }

  // It might be good to wait for this navigation to finish but given we're
  // navigating to about:blank it probably doesn't matter in practice.
  OnCreateActorTabComplete(*task, std::move(callback), journal_,
                           tabs::TabInterface::GetFromContents(contents));
}

base::WeakPtr<ActorKeyedService> ActorKeyedService::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

const std::map<TaskId, const ActorTask*> ActorKeyedService::GetActiveTasks()
    const {
  std::map<TaskId, const ActorTask*> active_tasks;
  for (const auto& [id, task] : active_tasks_) {
    CHECK_NE(task->IsCompleted(), true);
    active_tasks[id] = task.get();
  }
  return active_tasks;
}

size_t ActorKeyedService::GetActiveTasksCount() const {
  return active_tasks_.size();
}

void ActorKeyedService::ResetForTesting() {
  for (auto it = active_tasks_.begin(); it != active_tasks_.end();) {
    if (!it->second->IsCompleted()) {
      StopTask((it++)->first, ActorTask::StoppedReason::kTaskComplete);
    } else {
      ++it;
    }
  }
  active_tasks_.clear();
}

TaskId ActorKeyedService::CreateTask(
    const TaskSourceInfo& source_info,
    const EnterprisePolicyChecker* policy_checker) {
  return CreateTaskWithOptions(source_info, policy_checker, nullptr, nullptr);
}

TaskId ActorKeyedService::CreateTaskWithOptions(
    const TaskSourceInfo& source_info,
    const EnterprisePolicyChecker* policy_checker,
    webui::mojom::TaskOptionsPtr options,
    base::WeakPtr<ActorTaskDelegate> delegate,
    std::optional<glic::mojom::InvocationSource> initial_invocation_source) {
  return CreateTaskImpl(ui::NewUiEventDispatcher(GetActorUiStateManager()),
                        source_info, policy_checker, std::move(options),
                        std::move(delegate), initial_invocation_source);
}

TaskId ActorKeyedService::CreateTaskForTesting(
    std::unique_ptr<actor::ui::UiEventDispatcher> ui_event_dispatcher,
    const TaskSourceInfo& source_info,
    const EnterprisePolicyChecker* policy_checker,
    webui::mojom::TaskOptionsPtr options,
    base::WeakPtr<ActorTaskDelegate> delegate,
    std::optional<glic::mojom::InvocationSource> initial_invocation_source) {
  return CreateTaskImpl(std::move(ui_event_dispatcher), source_info,
                        policy_checker, std::move(options), std::move(delegate),
                        initial_invocation_source);
}

TaskId ActorKeyedService::CreateTaskImpl(
    std::unique_ptr<actor::ui::UiEventDispatcher> ui_event_dispatcher,
    const TaskSourceInfo& source_info,
    const EnterprisePolicyChecker* policy_checker,
    webui::mojom::TaskOptionsPtr options,
    base::WeakPtr<ActorTaskDelegate> delegate,
    std::optional<glic::mojom::InvocationSource> initial_invocation_source) {
  TRACE_EVENT0("actor", "ActorKeyedService::CreateTask");
  GetJournal().Log(GURL(), TaskId(), "ActorKeyedService::CreateTask", {});

  const TaskId task_id = next_task_id_.GenerateNextId();
  tabs::TabHandle initial_tab_handle = tabs::TabHandle::Null();
  if (options && options->actuation_tab_id.has_value()) {
    initial_tab_handle = tabs::TabHandle(options->actuation_tab_id.value());
  }

  auto actor_task = std::make_unique<ActorTask>(
      base::PassKey<ActorKeyedService>(), *this, task_id,
      std::move(ui_event_dispatcher), std::move(options), source_info,
      policy_checker, std::move(delegate), initial_invocation_source);

  if (initial_tab_handle != tabs::TabHandle::Null()) {
    actor_task->AddTab(initial_tab_handle, /*stop_task_on_detach=*/true,
                       base::DoNothing());
  }

  active_tasks_[task_id] = std::move(actor_task);

#if !BUILDFLAG(IS_ANDROID)
  actor_ui_state_manager_->LazyInitTabTracker();
#endif

  NotifyTaskStateChanged(*active_tasks_[task_id]);
  return task_id;
}

base::CallbackListSubscription ActorKeyedService::AddTaskStateChangedCallback(
    TaskStateChangedCallback callback) {
  return task_state_change_callback_list_.Add(std::move(callback));
}

void ActorKeyedService::NotifyTaskStateChanged(ActorTask& task) {
  if (task.IsCompleted()) {
    // Remove a stopped task from the active_tasks_ list. Post this since this
    // call comes from the ActorTask so we don't want to delete it while it's on
    // the stack.
    auto node = active_tasks_.extract(task.id());
    if (!node.empty()) {
      pending_delete_tasks_.insert(std::move(node));

      RunLater(base::BindOnce(
          [](base::WeakPtr<ActorKeyedService> self, TaskId task_id) {
            if (!self) {
              return;
            }
            self->pending_delete_tasks_.erase(task_id);
          },
          GetWeakPtr(), task.id()));
    }
  }

  task_state_change_callback_list_.Notify(task);
}

base::CallbackListSubscription
ActorKeyedService::AddTaskVisibilityChangedCallback(
    TaskVisibilityChangedCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  return task_visibility_change_callback_list_.Add(std::move(callback));
}

void ActorKeyedService::NotifyTaskVisibilityChanged(ActorTask& task) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  task_visibility_change_callback_list_.Notify(task);
}

base::CallbackListSubscription
ActorKeyedService::AddTaskStepProgressChangedCallback(
    TaskStepProgressChangedCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  return task_step_progress_change_callback_list_.Add(std::move(callback));
}

void ActorKeyedService::NotifyTaskStepProgressChanged(
    ActorTask& task,
    const std::string& step_progress) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  task_step_progress_change_callback_list_.Notify(task, step_progress);
}

void ActorKeyedService::RequestTabObservation(
    tabs::TabInterface& tab,
    TaskId task_id,
    std::optional<page_content_annotations::ScreenshotOptions::
                      ScreenshotCollectionOptions>
        screenshot_collection_options,
    base::OnceCallback<void(TabObservationResult)> callback) {
  TRACE_EVENT0("actor", "ActorKeyedService::RequestTabObservation");
  const GURL& last_committed_url = tab.GetContents()->GetLastCommittedURL();
  auto journal_entry = journal_.CreatePendingAsyncEntry(
      last_committed_url, task_id, MakeBrowserTrackUUID(task_id),
      "RequestTabObservation", {});
  page_content_annotations::FetchPageContextOptions options;

  if (!base::FeatureList::IsEnabled(actor::kGlicActorSkipScreenshot)) {
    options.screenshot_options =
        kFullPageScreenshot.Get()
            // It's safe to dereference the optional here because
            // kFullPageScreenshot being true implies
            // kGlicTabScreenshotPaintPreviewBackend is enabled.
            ? page_content_annotations::ScreenshotOptions::FullPage(
                  CreateOptionalPaintPreviewOptions().value(),
                  std::move(screenshot_collection_options))
            : page_content_annotations::ScreenshotOptions::ViewportOnly(
                  CreateOptionalPaintPreviewOptions(),
                  std::move(screenshot_collection_options));
  }

  options.annotated_page_content_options =
      optimization_guide::ActionableAIPageContentOptions(
          /* on_critical_path =*/true);
  // The maximum number of meta tags to extract from the page. This is a fairly
  // generous limit that should be sufficient for the metadata we expect to see.
  // 32 is the value specified in the TabObservation proto comment.
  options.annotated_page_content_options->max_meta_elements = 32;
  page_content_annotations::FetchPageContext(
      *tab.GetContents(), options,
      CreateActorJournalFetchPageProgressListener(journal_.GetSafeRef(),
                                                  last_committed_url, task_id),
      base::BindOnce(
          [](base::WeakPtr<tabs::TabInterface> tab,
             base::OnceCallback<void(TabObservationResult)> callback,
             std::unique_ptr<AggregatedJournal::PendingAsyncEntry>
                 pending_journal_entry,
             const GURL& last_committed_url,
             page_content_annotations::FetchPageContextResultCallbackArg
                 result) {
            if (base::FeatureList::IsEnabled(
                    kGlicActorFixPageObservationCrash)) {
              if (!result.has_value()) {
                std::move(callback).Run(base::unexpected(result.error()));
                return;
              }
            }

            if (result.has_value() &&
                result.value()->annotated_page_content_result.has_value()) {
              auto& fetch_result = **result;
              size_t size = fetch_result.annotated_page_content_result->proto
                                .ByteSizeLong();
              std::vector<uint8_t> buffer(size);
              fetch_result.annotated_page_content_result->proto
                  .SerializeToArray(buffer.data(), size);
              pending_journal_entry->GetJournal().LogAnnotatedPageContent(
                  last_committed_url, pending_journal_entry->GetTaskId(),
                  buffer);

              if (fetch_result.screenshot_result.has_value()) {
                auto& data = fetch_result.screenshot_result->screenshot_data;
                std::optional<std::vector<uint8_t>> iframe_data = std::nullopt;
                if (fetch_result.annotated_page_content_result->proto
                        .gemini_in_chrome_page_metadata()
                        .screenshot_info()
                        .iframe_info_size() > 0) {
                  iframe_data = actor::GetScreenshotWithIframeBoundingBoxes(
                      data, fetch_result.screenshot_result->mime_type,
                      fetch_result.annotated_page_content_result->proto
                          .gemini_in_chrome_page_metadata()
                          .screenshot_info());
                }
                pending_journal_entry->GetJournal().LogScreenshot(
                    last_committed_url, pending_journal_entry->GetTaskId(),
                    fetch_result.screenshot_result->mime_type,
                    base::as_byte_span(data), iframe_data);
              }

              if (tab) {
                actor::ActorTabData::From(tab.get())->DidObserveContent(
                    fetch_result.annotated_page_content_result->proto,
                    actor::ApcSource::kActor);
              }
            }

            std::move(callback).Run(std::move(result).value());
          },
          tab.GetWeakPtr(), std::move(callback), std::move(journal_entry),
          last_committed_url));
}

//  static
std::optional<std::string> ActorKeyedService::ExtractErrorMessageIfFailed(
    const TabObservationResult& result) {
  if (!result.has_value()) {
    return absl::StrFormat(
        "Failed Observation: code[%s] message[%s]",
        page_content_annotations::ToString(result.error().error_code),
        result.error().message);
  }

  page_content_annotations::FetchPageContextResult& fetch_result = **result;

  // Context for actor observations should always have an APC. It should also
  // have a screenshot unless it was skipped.
  bool has_apc = fetch_result.annotated_page_content_result.has_value();
  bool has_screenshot = fetch_result.screenshot_result.has_value();
  bool screenshot_required =
      !base::FeatureList::IsEnabled(actor::kGlicActorSkipScreenshot);
  if (!has_apc || (screenshot_required && !has_screenshot)) {
    return absl::StrFormat(
        "Fetch Error: APC[%s] screenshot[%s]",
        has_apc ? std::string("OK")
                : fetch_result.annotated_page_content_result.error(),
        has_screenshot ? std::string("OK")
                       : fetch_result.screenshot_result.error());
  }

  return std::nullopt;
}

void ActorKeyedService::PerformActions(
    TaskId task_id,
    std::vector<std::unique_ptr<ToolRequest>>&& actions,
    ActorTaskMetadata task_metadata,
    PerformActionsCallback callback) {
  TRACE_EVENT0("actor", "ActorKeyedService::PerformActions");
  std::vector<ActionResultWithLatencyInfo> empty_results;
  auto* task = GetTask(task_id);
  if (!task) {
    GetJournal().Log(GURL(), task_id, "ActorKeyedService::PerformActions",
                     JournalDetailsBuilder()
                         .Add("task_id", task_id)
                         .AddError("Invalid Task")
                         .Build());
    RunLater(
        base::BindOnce(std::move(callback),
                       MakeResultVector(mojom::ActionResultCode::kTaskWentAway),
                       TabObservationStrategy()));
    return;
  }

  if (actions.empty()) {
    GetJournal().Log(
        GURL(), task_id, "ActorKeyedService::PerformActions",
        JournalDetailsBuilder().AddError("Empty Actions List").Build());
    RunLater(base::BindOnce(
        std::move(callback),
        MakeResultVector(mojom::ActionResultCode::kEmptyActionSequence),
        TabObservationStrategy()));
    return;
  }

  task->GetExecutionEngine().AddWritableMainframeOrigins(
      task_metadata.added_writable_mainframe_origins());
  if (task_metadata.agent_container_config().has_value()) {
    JournalDetailsBuilder builder;
    if (!task->GetExecutionEngine()
             .origin_gating_checker()
             .actor_container_config_slot()
             .has_value()) {
      origin_gating::ActorContainerConfig config = ConvertAgentContainerConfig(
          task_metadata.agent_container_config().value());
      builder.Add("status", "assigned")
          .Add("active config", config.ToDebugValue());
      task->GetExecutionEngine()
          .origin_gating_checker()
          .actor_container_config_slot()
          .Assign(std::move(config));
    } else {
      builder.Add("status", "ignored config");
    }
    GetJournal().Log(GURL(), task_id, "ActorContainerConfigSlot::Assign",
                     std::move(builder).Build());
  }

  task->Act(
      std::move(actions),
      base::BindOnce(&ActorKeyedService::OnActionsFinished,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void ActorKeyedService::OnActionsFinished(
    PerformActionsCallback callback,
    std::vector<ActionResultWithLatencyInfo> action_results,
    TabObservationStrategy observation_strategy) {
  TRACE_EVENT0("actor", "ActorKeyedService::OnActionsFinished");

  if (base::FeatureList::IsEnabled(
          actor::kGlicPerformActionsReturnsBeforeStateChange)) {
    std::move(callback).Run(std::move(action_results),
                            std::move(observation_strategy));
  } else {
    // RunLater is load bearing. See:
    // https://chromium-review.googlesource.com/c/chromium/src/+/7552225/comment/b0b7f011_71da3233/
    RunLater(base::BindOnce(std::move(callback), std::move(action_results),
                            std::move(observation_strategy)));
  }
}

void ActorKeyedService::StopTask(TaskId task_id,
                                 ActorTask::StoppedReason stop_reason) {
  TRACE_EVENT0("actor", "ActorKeyedService::StopTask");
  GetJournal().Log(GURL(), task_id, "ActorKeyedService::StopTask",
                   JournalDetailsBuilder()
                       .Add("task_id", task_id)
                       .Add("stop_reason", stop_reason)
                       .Build());

  auto task_itr = active_tasks_.find(task_id);
  if (task_itr == active_tasks_.end()) {
    return;
  }

  task_itr->second->Stop(stop_reason);
}

ActorTask* ActorKeyedService::GetTask(TaskId task_id) {
  auto task = active_tasks_.find(task_id);
  if (task != active_tasks_.end()) {
    return task->second.get();
  }
  return nullptr;
}

ActorUiStateManagerInterface* ActorKeyedService::GetActorUiStateManager() {
  return actor_ui_state_manager_.get();
}

void ActorKeyedService::SetTabPendingActuation(tabs::TabHandle tab_handle) {
  if (actor_ui_state_manager_) {
    actor_ui_state_manager_->SetTabPendingActuation(tab_handle);
  }
}

bool ActorKeyedService::ClearTabPendingActuation(tabs::TabHandle tab_handle) {
  if (actor_ui_state_manager_) {
    return actor_ui_state_manager_->ClearTabPendingActuation(tab_handle);
  }
  return false;
}

bool ActorKeyedService::IsActiveOnTab(const tabs::TabInterface& tab) const {
  tabs::TabHandle handle = tab.GetHandle();
  for (auto [task_id, task] : GetActiveTasks()) {
    if (task->IsActingOnTab(handle)) {
      return true;
    }
  }

  return false;
}

ActorTask* ActorKeyedService::GetTaskFromTab(
    const tabs::TabInterface& tab) const {
  tabs::TabHandle handle = tab.GetHandle();
  for (const auto& [task_id, task] : active_tasks_) {
    if (task->HasTab(handle)) {
      return task.get();
    }
  }

  return nullptr;
}

Profile* ActorKeyedService::GetProfile() {
  return profile_;
}

std::vector<TaskId> ActorKeyedService::FindTaskIdsInActive(
    base::FunctionRef<bool(const ActorTask&)> predicate) const {
  std::vector<TaskId> result;
  for (const auto& [id, task] : active_tasks_) {
    if (predicate(*task)) {
      result.push_back(id);
    }
  }
  return result;
}

void ActorKeyedService::OnDownloadCreated(content::DownloadManager* manager,
                                          download::DownloadItem* item) {
  if (content::WebContents* web_contents =
          content::DownloadItemUtils::GetWebContents(item)) {
    if (const ActorTask* task =
            GetActingActorTaskForWebContents(web_contents)) {
      RecordDirectDownloadTriggered(true);
      int64_t navigation_id =
          web_contents->GetPrimaryMainFrame()
              ? web_contents->GetPrimaryMainFrame()->GetNavigationId()
              : 0;
      ActorCriticalActionLogger::LogAgentSelfReportedAction(
          profile_, task->source_info().id.value_or(""),
          critical_actions::ActionType::kDownload, item->GetURL(),
          navigation_id, task->id());
    }
  }
}

void ActorKeyedService::AddObserver(BackgroundActuationObserver* observer) {
  observers_.AddObserver(observer);
}

void ActorKeyedService::RemoveObserver(BackgroundActuationObserver* observer) {
  observers_.RemoveObserver(observer);
}

void ActorKeyedService::NotifyBackgroundTabReady(
    tabs::TabInterface* tab,
    const std::string& glic_trigger_message_id) {
  for (auto& observer : observers_) {
    observer.OnBackgroundTabPrepared(tab, glic_trigger_message_id);
  }
}

void ActorKeyedService::NotifyBackgroundSetupFailed(
    const std::string& glic_trigger_message_id) {
  for (auto& observer : observers_) {
    observer.OnBackgroundSetupFailed(glic_trigger_message_id);
  }
}

base::CallbackListSubscription
ActorKeyedService::AddMessageTriggerTaskStoppedCallback(
    MessageTriggerTaskStoppedCallback callback) {
  return message_trigger_task_stopped_callbacks_.Add(std::move(callback));
}

void ActorKeyedService::OnMessageTriggerTaskStopped(
    const std::string& message_id) {
  message_trigger_task_stopped_callbacks_.Notify(message_id);
}

#if BUILDFLAG(IS_ANDROID)
base::CallbackListSubscription
ActorKeyedService::AddForegroundServiceStartedCallback(
    EnsureForegroundServiceStartedCallback callback) {
  return ensure_foreground_service_started_callbacks_.Add(std::move(callback));
}

void ActorKeyedService::EnsureForegroundServiceStarted(
    const std::string& glic_trigger_message_id) {
  ensure_foreground_service_started_callbacks_.Notify(glic_trigger_message_id);
}
#endif

}  // namespace actor
