// Copyright 2026 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/glic/selection/explain_selection_trigger.h"

#include <utility>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/glic/public/features.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/generated_resources.h"
#include "components/optimization_guide/core/model_execution/feature_keys.h"
#include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h"
#include "components/optimization_guide/core/model_execution/remote_model_executor.h"
#include "components/optimization_guide/core/optimization_guide_util.h"
#include "components/optimization_guide/proto/string_value.pb.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

namespace glic {

ExplainSelectionTrigger::ExplainSelectionTrigger() = default;

ExplainSelectionTrigger::~ExplainSelectionTrigger() = default;

// static
std::string ExplainSelectionTrigger::GetPromptTemplate() {
  const auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch("glic-inline-prompt")) {
    return command_line->GetSwitchValueASCII("glic-inline-prompt");
  }
  if (command_line->HasSwitch("glic-inline-prompt-file")) {
    base::FilePath prompt_file =
        command_line->GetSwitchValuePath("glic-inline-prompt-file");
    std::string file_content;
    if (base::ReadFileToString(prompt_file, &file_content)) {
      return std::string(
          base::TrimWhitespaceASCII(file_content, base::TRIM_ALL));
    }
  }

  std::string param_prompt =
      features::kGlicSelectionPromptInlinePromptTemplate.Get();
  if (!param_prompt.empty()) {
    return param_prompt;
  }

  // TODO(b/539514187): Load default prompt template from Chrome branded and
  // translated resource strings when Finch parameter is not specified.
  return "";
}

// static
std::string ExplainSelectionTrigger::GetGeminiApiKey() {
  const auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch("glic-gemini-api-key")) {
    return command_line->GetSwitchValueASCII("glic-gemini-api-key");
  }
  return "";
}

// static
bool ExplainSelectionTrigger::IsInlineFulfillmentSupported() {
  if (!features::kGlicSelectionPromptInlineFulfillment.Get()) {
    return false;
  }
  return !GetPromptTemplate().empty();
}

// static
bool ExplainSelectionTrigger::ShouldUseOptimizationGuide() {
  const auto* command_line = base::CommandLine::ForCurrentProcess();
  return command_line->HasSwitch("glic-inline-use-optimization-guide");
}

// static
std::string ExplainSelectionTrigger::FormatPrompt(
    const std::string& prompt_template,
    const std::string& selected_text,
    const std::string& surrounding_text) {
  std::string formatted_prompt = prompt_template;
  size_t pos = formatted_prompt.find("$1");
  if (pos != std::string::npos) {
    formatted_prompt.replace(pos, 2, selected_text);
  } else {
    formatted_prompt += "\n" + selected_text;
  }

  size_t pos2 = formatted_prompt.find("$2");
  if (pos2 != std::string::npos) {
    formatted_prompt.replace(pos2, 2, surrounding_text);
  }
  return formatted_prompt;
}

void ExplainSelectionTrigger::RequestExplanation(
    content::WebContents* web_contents,
    const std::string& selected_text,
    const std::string& surrounding_text,
    StreamUpdateCallback callback) {
  if (!web_contents || !callback) {
    if (callback) {
      callback.Run("", true, "Invalid web contents");
    }
    return;
  }

  if (!IsInlineFulfillmentSupported()) {
    callback.Run("", true,
                 "Inline fulfillment disabled or private prompt resource "
                 "unavailable");
    return;
  }

  start_time_ = base::TimeTicks::Now();

  // TODO(b/539511437): Evaluate whether enterprise Data Loss Prevention (DLP)
  // restrictions apply before sending selected text to the Gemini model.

  std::string prompt =
      FormatPrompt(GetPromptTemplate(), selected_text, surrounding_text);

  if (ShouldUseOptimizationGuide()) {
    SendOptimizationGuideRequest(web_contents, prompt, callback);
  } else {
    SendGeminiApiRequest(web_contents, prompt, selected_text, callback);
  }
}

void ExplainSelectionTrigger::SendOptimizationGuideRequest(
    content::WebContents* web_contents,
    const std::string& prompt,
    StreamUpdateCallback callback) {
  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  if (!profile) {
    callback.Run(/*markdown_output=*/"", /*is_complete=*/true,
                 /*error_message=*/"Invalid profile");
    return;
  }

  OptimizationGuideKeyedService* optimization_guide_keyed_service =
      OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
  if (!optimization_guide_keyed_service) {
    callback.Run(
        /*markdown_output=*/"", /*is_complete=*/true,
        /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE));
    return;
  }

  optimization_guide::proto::StringValue request_metadata;
  request_metadata.set_value(prompt);

  optimization_guide_keyed_service->ExecuteModel(
      optimization_guide::ModelBasedCapabilityKey::kTest, request_metadata,
      /*options=*/{},
      base::BindOnce(&ExplainSelectionTrigger::OnOptimizationGuideResponse,
                     weak_ptr_factory_.GetWeakPtr(), callback));
}

void ExplainSelectionTrigger::OnOptimizationGuideResponse(
    StreamUpdateCallback callback,
    optimization_guide::OptimizationGuideModelExecutionResult result,
    std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry) {
  base::TimeDelta latency = base::TimeTicks::Now() - start_time_;
  base::UmaHistogramTimes(
      "OptimizationGuide.ModelExecution.ExplainSelection.TTFT", latency);
  base::UmaHistogramTimes(
      "OptimizationGuide.ModelExecution.ExplainSelection.TotalLatency",
      latency);

  if (!result.response.has_value()) {
    callback.Run(
        /*markdown_output=*/"", /*is_complete=*/true,
        /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE));
    return;
  }

  std::optional<optimization_guide::proto::StringValue> response =
      optimization_guide::ParsedAnyMetadata<
          optimization_guide::proto::StringValue>(result.response.value());
  if (!response || response->value().empty()) {
    callback.Run(
        /*markdown_output=*/"", /*is_complete=*/true,
        /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE));
    return;
  }

  callback.Run(response->value(), /*is_complete=*/true, /*error_message=*/"");
}

void ExplainSelectionTrigger::SendGeminiApiRequest(
    content::WebContents* web_contents,
    const std::string& formatted_prompt,
    const std::string& selected_text,
    StreamUpdateCallback callback) {
  std::string api_key = GetGeminiApiKey();

  // Construct Gemini API generateContent payload
  base::DictValue payload;
  base::ListValue contents;
  base::DictValue user_content;
  user_content.Set("role", "user");

  base::ListValue parts;
  base::DictValue text_part;
  text_part.Set("text", formatted_prompt);
  parts.Append(std::move(text_part));

  user_content.Set("parts", std::move(parts));
  contents.Append(std::move(user_content));
  payload.Set("contents", std::move(contents));

  base::DictValue gen_config;
  gen_config.Set("temperature", 0.2);
  gen_config.Set("maxOutputTokens", 500);
  payload.Set("generationConfig", std::move(gen_config));

  std::string request_body;
  base::JSONWriter::Write(payload, &request_body);

  std::string model_name = "gemini-flash-lite-latest";
  const auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch("glic-gemini-model")) {
    model_name = command_line->GetSwitchValueASCII("glic-gemini-model");
  }

  auto resource_request = std::make_unique<network::ResourceRequest>();
  std::string url_str =
      "https://generativelanguage.googleapis.com/v1beta/models/" + model_name +
      ":generateContent";

  if (!api_key.empty()) {
    url_str += "?key=" + api_key;
    resource_request->headers.SetHeader("X-Goog-Api-Key", api_key);
  }

  resource_request->url = GURL(url_str);
  resource_request->method = "POST";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("glic_selection_ask_gemini_api", R"(
        semantics {
          sender: "Gemini in Chrome"
          description:
            "Sends selected text on a web page to the Gemini API to request "
            "an inline explanation response rendered directly on the selection "
            "widget."
          trigger: "User clicks the Ask Gemini button on text selection widget."
          data:
            "The text selected by the user on the web page and the prompt "
            "template used for generating the inline explanation."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              owners: "//chrome/browser/glic/OWNERS"
            }
          }
          user_data {
            type: USER_CONTENT
            type: WEB_CONTENT
            type: ACCESS_TOKEN
          }
          last_reviewed: "2026-07-23"
        }
        policy {
          cookies_allowed: NO
          setting:
            "Users can enable or disable this feature via Chrome flags or "
            "settings for Glic selection prompt features."
          chrome_policy {
            GeminiSettings {
              GeminiSettings: 1
            }
            GenAiDefaultSettings {
              GenAiDefaultSettings: 2
            }
          }
        })");

  url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                 traffic_annotation);
  url_loader_->AttachStringForUpload(request_body, "application/json");
  url_loader_->SetAllowHttpErrorResults(true);

  auto url_loader_factory =
      web_contents->GetBrowserContext()
          ->GetDefaultStoragePartition()
          ->GetURLLoaderFactoryForBrowserProcess();

  constexpr size_t kMaxGeminiResponseSizeBytes = 1024 * 1024;  // 1 MB
  url_loader_->DownloadToString(
      url_loader_factory.get(),
      base::BindOnce(&ExplainSelectionTrigger::OnGeminiApiResponse,
                     weak_ptr_factory_.GetWeakPtr(), callback, selected_text),
      kMaxGeminiResponseSizeBytes);
}

void ExplainSelectionTrigger::OnGeminiApiResponse(
    StreamUpdateCallback callback,
    std::string selected_text,
    std::optional<std::string> response_body) {
  base::TimeDelta latency = base::TimeTicks::Now() - start_time_;
  base::UmaHistogramTimes(
      "OptimizationGuide.ModelExecution.ExplainSelection.TTFT", latency);
  base::UmaHistogramTimes(
      "OptimizationGuide.ModelExecution.ExplainSelection.TotalLatency",
      latency);

  if (!response_body || response_body->empty()) {
    callback.Run(/*text=*/"", /*is_complete=*/true,
                 /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE));
    return;
  }

  std::optional<base::DictValue> parsed =
      base::JSONReader::ReadDict(*response_body, base::JSON_PARSE_RFC);
  if (parsed) {
    const base::ListValue* candidates = parsed->FindList("candidates");
    if (candidates && !candidates->empty() && (*candidates)[0].is_dict()) {
      const base::DictValue& first_candidate = (*candidates)[0].GetDict();
      const base::DictValue* content = first_candidate.FindDict("content");
      if (content) {
        const base::ListValue* parts = content->FindList("parts");
        if (parts && !parts->empty() && (*parts)[0].is_dict()) {
          const std::string* text = (*parts)[0].GetDict().FindString("text");
          if (text && !text->empty()) {
            callback.Run(*text, /*is_complete=*/true, /*error_message=*/"");
            return;
          }
        }
      }
    }

    const base::DictValue* error_dict = parsed->FindDict("error");
    if (error_dict) {
      const std::string* err_msg = error_dict->FindString("message");
      if (err_msg && !err_msg->empty()) {
        std::string err_out = "**Gemini API Error:** " + *err_msg;
        callback.Run(err_out, /*is_complete=*/true, /*error_message=*/"");
        return;
      }
    }
  }

  callback.Run(/*text=*/"", /*is_complete=*/true,
               /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE));
}

}  // namespace glic
