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

#include "extensions/browser/api/alarms/alarms_api.h"

#include <stddef.h>

#include <utility>

#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/values.h"
#include "extensions/browser/api/alarms/alarm_manager.h"
#include "extensions/browser/api/alarms/alarms_api_constants.h"
#include "extensions/common/api/alarms.h"
#include "extensions/common/error_utils.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"

namespace extensions {

namespace alarms = api::alarms;

namespace {

constexpr char kDefaultAlarmName[] = "";
constexpr char kBothRelativeAndAbsoluteTime[] =
    "Cannot set both when and delayInMinutes.";
constexpr char kAlarmNameBothWaysError[] =
    "Cannot set alarm name in both separate argument and object form.";
constexpr char kNoScheduledTime[] =
    "Must set at least one of when, delayInMinutes, or periodInMinutes.";
constexpr char kMaxAlarmsError[] =
    "An extension cannot have more than %d active alarms.";

constexpr char kWarningMinimumDevDelay[] =
    "Alarm %s is less than the minimum duration of %zu %s."
    " In packed extensions, alarm \"%s\" will fire after the minimum duration.";

constexpr char kWarningMinimumReleaseDelay[] =
    "Alarm %s is less than the minimum duration of %zu %s."
    " Alarm \"%s\" will fire after the minimum duration.";

constexpr size_t kMaximumNameLength = 1024;

constexpr char kErrorMaximumNameLength[] =
    "Alarm name size is %zu bytes which exceeds the limit of %zu bytes.";

bool ValidateAlarmCreateInfo(const alarms::AlarmCreateInfo& create_info,
                             const Extension* extension,
                             std::string* error,
                             std::vector<std::string>& warnings) {
  if (create_info.delay_in_minutes && create_info.when) {
    *error = kBothRelativeAndAbsoluteTime;
    return false;
  }
  if (!create_info.delay_in_minutes.has_value() &&
      !create_info.when.has_value() &&
      !create_info.period_in_minutes.has_value()) {
    *error = kNoScheduledTime;
    return false;
  }
  if (create_info.name->length() > kMaximumNameLength) {
    *error = base::StringPrintf(kErrorMaximumNameLength,
                                create_info.name->length(), kMaximumNameLength);
    return false;
  }

  // Users can always use an absolute timeout to request an arbitrarily-short or
  // negative delay.  We won't honor the short timeout, but we can't check it
  // and warn the user because it would introduce race conditions (say they
  // compute a long-enough timeout, but then the call into the alarms interface
  // gets delayed past the boundary).  However, it's still worth warning about
  // relative delays that are shorter than we'll honor.
  // For this minimum delay, we compare to the minimum of a packed extension,
  // since we want to appropriately warn developers (we take into account the
  // "real" unpacked state in the phrasing of the warning).
  const base::TimeDelta min_packed_delay =
      alarms_api_constants::GetMinimumDelay(/*is_unpacked=*/false,
                                            extension->manifest_version());
  const bool is_unpacked = Manifest::IsUnpackedLocation(extension->location());
  if (create_info.delay_in_minutes) {
    if (base::Minutes(*create_info.delay_in_minutes) < min_packed_delay) {
      if (is_unpacked && extension->manifest_version() == 2) {
        warnings.push_back(base::StringPrintf(
            kWarningMinimumDevDelay, "delay", min_packed_delay.InSeconds(),
            min_packed_delay.InSeconds() == 1 ? "second" : "seconds",
            create_info.name->c_str()));
      } else {
        // Manifest V3 has the same delay for packed and unpacked extensions, so
        // it is fine to use the packed delay here even though we may be in this
        // case for an unpacked MV3 item.
        warnings.push_back(base::StringPrintf(
            kWarningMinimumReleaseDelay, "delay", min_packed_delay.InSeconds(),
            min_packed_delay.InSeconds() == 1 ? "second" : "seconds",
            create_info.name->c_str()));
      }
    }
  }
  if (create_info.period_in_minutes) {
    if (base::Minutes(*create_info.period_in_minutes) < min_packed_delay) {
      if (is_unpacked && extension->manifest_version() == 2) {
        warnings.push_back(base::StringPrintf(
            kWarningMinimumDevDelay, "period", min_packed_delay.InSeconds(),
            min_packed_delay.InSeconds() == 1 ? "second" : "seconds",
            create_info.name->c_str()));
      } else {
        // Manifest V3 has the same delay for packed and unpacked extensions, so
        // it is fine to use the packed delay here even though we may be in this
        // case for an unpacked MV3 item.
        warnings.push_back(base::StringPrintf(
            kWarningMinimumReleaseDelay, "period", min_packed_delay.InSeconds(),
            min_packed_delay.InSeconds() == 1 ? "second" : "seconds",
            create_info.name->c_str()));
      }
    }
  }

  return true;
}

}  // namespace

AlarmsCreateFunction::AlarmsCreateFunction()
    : clock_(base::DefaultClock::GetInstance()) {}

AlarmsCreateFunction::AlarmsCreateFunction(base::Clock* clock)
    : clock_(clock) {}

AlarmsCreateFunction::~AlarmsCreateFunction() = default;

ExtensionFunction::ResponseAction AlarmsCreateFunction::Run() {
  std::optional<alarms::Create::Params> params =
      alarms::Create::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  // Do not permit alarm name to be specified in two places.
  if (params->name.has_value() && params->alarm_info.name.has_value()) {
    return RespondNow(Error(kAlarmNameBothWaysError));
  }

  AlarmManager* const alarm_manager = AlarmManager::Get(browser_context());
  EXTENSION_FUNCTION_VALIDATE(alarm_manager);

  if (alarm_manager->GetCountForExtension(extension_id()) >=
      AlarmManager::kMaxAlarmsPerExtension) {
    return RespondNow(Error(base::StringPrintf(
        kMaxAlarmsError, AlarmManager::kMaxAlarmsPerExtension)));
  }

  if (!params->alarm_info.name.has_value()) {
    params->alarm_info.name = params->name.value_or(kDefaultAlarmName);
  }

  std::vector<std::string> warnings;
  std::string error;
  if (!ValidateAlarmCreateInfo(params->alarm_info, extension(), &error,
                               warnings)) {
    return RespondNow(Error(std::move(error)));
  }
  for (const std::string& warning : warnings) {
    WriteToConsole(blink::mojom::ConsoleMessageLevel::kWarning, warning);
  }

  base::TimeDelta granularity = alarms_api_constants::GetMinimumDelay(
      Manifest::IsUnpackedLocation(extension()->location()),
      extension()->manifest_version());
  Alarm alarm(params->alarm_info, granularity, clock_->Now());
  alarm_manager->AddAlarm(
      extension_id(), std::move(alarm),
      base::BindOnce(&AlarmsCreateFunction::Callback, this));

  // AddAlarm might have already responded.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void AlarmsCreateFunction::Callback() {
  Respond(NoArguments());
}

ExtensionFunction::ResponseAction AlarmsGetFunction::Run() {
  std::optional<alarms::Get::Params> params =
      alarms::Get::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string name = params->name.value_or(kDefaultAlarmName);
  AlarmManager::Get(browser_context())
      ->GetAlarm(extension_id(), name,
                 base::BindOnce(&AlarmsGetFunction::Callback, this));

  // GetAlarm might have already responded.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void AlarmsGetFunction::Callback(extensions::Alarm* alarm) {
  if (alarm) {
    Respond(ArgumentList(alarms::Get::Results::Create(*alarm->js_alarm)));
  } else {
    Respond(NoArguments());
  }
}

ExtensionFunction::ResponseAction AlarmsGetAllFunction::Run() {
  AlarmManager::Get(browser_context())
      ->GetAllAlarms(extension_id(),
                     base::BindOnce(&AlarmsGetAllFunction::Callback, this));
  // GetAllAlarms might have already responded.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void AlarmsGetAllFunction::Callback(const AlarmList* alarms) {
  base::ListValue alarms_value;
  if (alarms) {
    for (const auto& alarm : *alarms)
      alarms_value.Append(alarm.js_alarm->ToValue());
  }
  Respond(WithArguments(std::move(alarms_value)));
}

ExtensionFunction::ResponseAction AlarmsClearFunction::Run() {
  std::optional<alarms::Clear::Params> params =
      alarms::Clear::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string name = params->name.value_or(kDefaultAlarmName);
  AlarmManager::Get(browser_context())
      ->RemoveAlarm(extension_id(), name,
                    base::BindOnce(&AlarmsClearFunction::Callback, this));

  // RemoveAlarm might have already responded.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void AlarmsClearFunction::Callback(bool removed) {
  Respond(WithArguments(removed));
}

ExtensionFunction::ResponseAction AlarmsClearAllFunction::Run() {
  AlarmManager::Get(browser_context())
      ->RemoveAllAlarms(
          extension_id(),
          base::BindOnce(&AlarmsClearAllFunction::Callback, this));
  // RemoveAllAlarms might have already responded.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void AlarmsClearAllFunction::Callback() {
  Respond(WithArguments(true));
}

}  // namespace extensions
