// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/file_handlers/web_file_handlers_permission_handler.h"

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

#include "base/files/file_path.h"
#include "base/files/safe_base_name.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/message_formatter.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/grit/generated_resources.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/pref_types.h"
#include "extensions/common/extension.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_provider.h"
#include "extensions/common/manifest_handlers/web_file_handlers_info.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/dialog_delegate.h"

DEFINE_ELEMENT_IDENTIFIER_VALUE(kWebFileHandlersFileLaunchDialogCheckbox);

namespace extensions {

namespace {

// The remembered choice is stored as a dictionary keyed by extension ID. It
// records the user's decision together with the set of file extensions and
// MIME types that were declared by the extension at the time the choice was
// made, so the choice can be re-evaluated if the declared handlers later
// change.
constexpr PrefMap kPrefShouldOpen{"web_file_handlers_should_open",
                                  PrefType::kDictionary,
                                  PrefScope::kExtensionSpecific};

constexpr char kPrefShouldOpenKey[] = "should_open";
constexpr char kPrefFileExtensionsKey[] = "file_extensions";
constexpr char kPrefMimeTypesKey[] = "mime_types";

// Get extension prefs for profile.
ExtensionPrefs* GetExtensionPrefs(Profile* profile) {
  return ExtensionPrefs::Get(profile);
}

// Returns true if every entry in `current` is present in the stored `list`.
bool StoredListContainsAll(const base::ListValue* list,
                           const std::set<std::string>& current) {
  if (!list) {
    return current.empty();
  }
  for (const std::string& entry : current) {
    bool found = false;
    for (const base::Value& stored_val : *list) {
      if (stored_val.is_string() && stored_val.GetString() == entry) {
        found = true;
        break;
      }
    }
    if (!found) {
      return false;
    }
  }
  return true;
}

base::ListValue SetToListValue(const std::set<std::string>& values) {
  base::ListValue list;
  for (const std::string& value : values) {
    list.Append(value);
  }
  return list;
}

// Returns the remembered choice if one was stored and the file handlers
// currently declared by the extension are a subset of those that were declared
// when the choice was made.
std::optional<bool> GetRememberedChoice(
    ExtensionPrefs* extension_prefs,
    const ExtensionId& extension_id,
    const apps::FileHandlers& current_handlers) {
  const base::DictValue* dict =
      extension_prefs->ReadPrefAsDictionary(extension_id, kPrefShouldOpen);
  if (!dict) {
    return std::nullopt;
  }
  std::optional<bool> should_open = dict->FindBool(kPrefShouldOpenKey);
  if (!should_open.has_value()) {
    return std::nullopt;
  }
  if (!StoredListContainsAll(
          dict->FindList(kPrefFileExtensionsKey),
          apps::GetFileExtensionsFromFileHandlers(current_handlers)) ||
      !StoredListContainsAll(
          dict->FindList(kPrefMimeTypesKey),
          apps::GetMimeTypesFromFileHandlers(current_handlers))) {
    return std::nullopt;
  }
  return should_open;
}

class WebFileHandlersFileLaunchDialogDelegate : public ui::DialogModelDelegate {
 public:
  explicit WebFileHandlersFileLaunchDialogDelegate(
      base::OnceCallback<void(bool, bool)> callback,
      ui::ElementIdentifier checkbox_identifier)
      : callback_(std::move(callback)),
        checkbox_identifier_(checkbox_identifier) {}

  ~WebFileHandlersFileLaunchDialogDelegate() override = default;

  // Determine whether to open the file and maybe remember the decision.
  void OnDialogAccepted() { Run(/*should_open=*/true); }

  void OnDialogClosed() { Run(/*should_open=*/false); }

  // If escape is pressed, neither open the file nor remember the decision.
  void SetActionCloseCallback() {
    std::move(callback_).Run(/*should_open=*/false, /*should_remember=*/false);
  }

 private:
  base::OnceCallback<void(bool, bool)> callback_;

  void Run(bool should_open) {
    auto* checkbox =
        this->dialog_model()->GetCheckboxByUniqueId(checkbox_identifier_);
    bool should_remember = checkbox->is_checked();
    std::move(callback_).Run(should_open, should_remember);
  }

  // The element identifier is declared once by this parent and reused here.
  ui::ElementIdentifier checkbox_identifier_;
};

}  // namespace

// static
bool WebFileHandlersPermissionHandler::remember_selection_ = false;

// Open file using Web File Handlers. Maybe show a file launch dialog first.
WebFileHandlersPermissionHandler::WebFileHandlersPermissionHandler(
    Profile* profile)
    : profile_(profile) {}

WebFileHandlersPermissionHandler::~WebFileHandlersPermissionHandler() = default;

base::AutoReset<bool>
WebFileHandlersPermissionHandler::SetRememberSelectionForTesting(
    bool remember_selection) {
  remember_selection_ = remember_selection;
  return base::AutoReset<bool>(&remember_selection_, remember_selection);
}

void WebFileHandlersPermissionHandler::Confirm(
    const Extension& extension,
    const std::vector<base::SafeBaseName>& base_names,
    CallbackType launch_callback) {
  CHECK(!base_names.empty());

  // Default installed extensions can skip the file launch dialog.
  // TODO(crbug.com/40269541): Remove the allowlist check after development.
  // Also, for development and manual testing purposes, the manifest_features
  // allowlist can also bypass the dialog.
  if (WebFileHandlers::CanBypassPermissionDialog(extension)) {
    std::move(launch_callback).Run(/*should_open=*/true);
    return;
  }

  auto apps_file_handlers = GetAppsFileHandlers(extension);

  // Get saved preferences that represents previous file opening decisions.
  const auto remembered_choice = GetRememberedChoice(
      GetExtensionPrefs(profile_), extension.id(), apps_file_handlers);
  if (remembered_choice.has_value()) {
    std::move(launch_callback).Run(remembered_choice.value());
    return;
  }

  std::set<std::string> file_extensions =
      apps::GetFileExtensionsFromFileHandlers(apps_file_handlers);
  std::set<std::string> mime_types =
      apps::GetMimeTypesFromFileHandlers(apps_file_handlers);
  const std::vector<std::u16string> file_types =
      web_app::TransformFileExtensionsForDisplay(file_extensions);

  // Maybe open the file. Maybe remember the decision to open the file.
  auto callback_after_dialog = base::BindOnce(
      &WebFileHandlersPermissionHandler::CallbackAfterDialog,
      weak_factory_.GetWeakPtr(), extension.id(), std::move(file_extensions),
      std::move(mime_types), std::move(launch_callback));

  // Present a contextual file launch dialog.
  ShowFileLaunchDialog(std::move(base_names), file_types,
                       std::move(callback_after_dialog));
}

// static
void WebFileHandlersPermissionHandler::ShowFileLaunchDialog(
    const std::vector<base::SafeBaseName>& base_names,
    const std::vector<std::u16string>& file_types,
    base::OnceCallback<void(bool, bool)> callback) {
  auto checkbox_id =
      ui::ElementIdentifier(kWebFileHandlersFileLaunchDialogCheckbox);

  auto bubble_delegate_unique =
      std::make_unique<WebFileHandlersFileLaunchDialogDelegate>(
          std::move(callback), checkbox_id);
  WebFileHandlersFileLaunchDialogDelegate* bubble_delegate =
      bubble_delegate_unique.get();

  std::u16string title = base::i18n::MessageFormatter::FormatWithNamedArgs(
      l10n_util::GetStringUTF16(IDS_EXTENSIONS_WFH_PERMISSION_HANDLER_FILES),
      "FILE_COUNT", static_cast<int>(base_names.size()), "FILE1",
      base_names[0].path().value());

  // Prepare every file extension for display in the checkbox.
  const auto file_types_for_display = base::JoinString(
      file_types,
      l10n_util::GetStringUTF16(IDS_WEB_APP_FILE_HANDLING_LIST_SEPARATOR));
  ui::DialogModelLabel checkbox_label =
      ui::DialogModelLabel(base::i18n::MessageFormatter::FormatWithNamedArgs(
          l10n_util::GetStringUTF16(
              IDS_WEB_APP_FILE_HANDLING_DIALOG_STICKY_CHOICE),
          "FILE_TYPE_COUNT", static_cast<int>(file_types.size()), "FILE_TYPES",
          file_types_for_display));

  // Prepare checkbox for testing.
  ui::DialogModelCheckbox::Params checkbox_params;
  checkbox_params.SetIsChecked(remember_selection_);
  checkbox_params.SetVisible(true);

  // TODO(crbug.com/40269541): Add extension name and icon. Show files. Design:
  // https://docs.google.com/document/d/1h7ZjryB2zYEjUG9DqPLzAM1iSUXr8ZadUUY02ycExAQ
  std::unique_ptr<ui::DialogModel> dialog_model =
      ui::DialogModel::Builder(std::move(bubble_delegate_unique))
          .SetInternalName("WebFileHandlersFileLaunchDialogView")
          .SetTitle(title)
          .AddCheckbox(checkbox_id, checkbox_label, checkbox_params)
          .AddOkButton(
              base::BindOnce(
                  &WebFileHandlersFileLaunchDialogDelegate::OnDialogAccepted,
                  base::Unretained(bubble_delegate)),
              ui::DialogModel::Button::Params().SetLabel(
                  l10n_util::GetStringUTF16(
                      IDS_WEB_APP_FILE_HANDLING_POSITIVE_BUTTON)))
          .AddCancelButton(
              base::BindOnce(
                  &WebFileHandlersFileLaunchDialogDelegate::OnDialogClosed,
                  base::Unretained(bubble_delegate)),
              ui::DialogModel::Button::Params().SetLabel(
                  l10n_util::GetStringUTF16(
                      IDS_WEB_APP_FILE_HANDLING_NEGATIVE_BUTTON)))
          .SetCloseActionCallback(base::BindOnce(
              &WebFileHandlersFileLaunchDialogDelegate::SetActionCloseCallback,
              base::Unretained(bubble_delegate)))
          .Build();

  std::unique_ptr<views::BubbleDialogModelHost> dialog =
      views::BubbleDialogModelHost::CreateModal(std::move(dialog_model),
                                                ui::mojom::ModalType::kWindow);
  dialog->SetOwnedByWidget(views::WidgetDelegate::OwnedByWidgetPassKey());
  views::Widget* modal_dialog = views::DialogDelegate::CreateDialogWidget(
      std::move(dialog), /*context=*/nullptr, /*parent=*/nullptr);
  modal_dialog->Show();
}

void WebFileHandlersPermissionHandler::CallbackAfterDialog(
    const ExtensionId& extension_id,
    const std::set<std::string>& file_extensions,
    const std::set<std::string>& mime_types,
    CallbackType launch_callback,
    bool should_open,
    bool should_remember) {
  // Maybe remember the decision to open the file.
  if (should_remember) {
    base::DictValue dict;
    dict.Set(kPrefShouldOpenKey, should_open);
    dict.Set(kPrefFileExtensionsKey, SetToListValue(file_extensions));
    dict.Set(kPrefMimeTypesKey, SetToListValue(mime_types));
    GetExtensionPrefs(profile_)->SetDictionaryPref(
        extension_id, kPrefShouldOpen, std::move(dict));
  }

  // Maybe open the file.
  std::move(launch_callback).Run(should_open);
}

// static
const apps::FileHandlers WebFileHandlersPermissionHandler::GetAppsFileHandlers(
    const Extension& extension) {
  apps::FileHandlers web_file_handlers;
  auto* file_handlers = WebFileHandlers::GetFileHandlers(extension);

  if (!file_handlers) {
    return web_file_handlers;
  }

  for (const auto& web_file_handler : *file_handlers) {
    apps::FileHandler file_handler;
    file_handler.action = GURL(web_file_handler.file_handler.action);
    file_handler.display_name =
        base::UTF8ToUTF16(web_file_handler.file_handler.name);

    // Compute `accept`, which contains all mime types and file extensions.
    apps::FileHandler::Accept accept;
    for (const auto [mime_type, file_extension_list] :
         web_file_handler.file_handler.accept.additional_properties) {
      apps::FileHandler::AcceptEntry accept_entry;
      accept_entry.mime_type = mime_type;
      auto file_extensions = base::MakeFlatSet<std::string>(
          file_extension_list.GetList(), /*comp=*/{},
          [&](const auto& file_extension) {
            return file_extension.GetString();
          });
      accept_entry.file_extensions = file_extensions;
      accept.emplace_back(accept_entry);
    }
    file_handler.accept = accept;

    // The default launch type is single client.
    file_handler.launch_type =
        web_file_handler.launch_type ==
                WebFileHandler::LaunchType::kSingleClient
            ? apps::FileHandler::LaunchType::kSingleClient
            : apps::FileHandler::LaunchType::kMultipleClients;

    web_file_handlers.emplace_back(file_handler);
  }

  return web_file_handlers;
}

}  // namespace extensions
