// 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 "third_party/blink/renderer/modules/ai/exception_helpers.h"

#include "base/debug/dump_without_crashing.h"
#include "base/notreached.h"
#include "third_party/blink/public/mojom/ai/ai_common.mojom-blink.h"
#include "third_party/blink/public/mojom/ai/ai_manager.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/ai/ai_manager.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_quota_exceeded_error_options.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/quota_exceeded_error.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/casting.h"

namespace blink {

const char kExceptionMessageExecutionContextInvalid[] =
    "The execution context is not valid.";
const char kExceptionMessageServiceUnavailable[] =
    "Model execution service is not available.";
const char kExceptionMessageDocumentNotActive[] = "The document is not active.";

const char kExceptionMessagePermissionDenied[] =
    "A user permission error occurred, such as not signed-in or not "
    "allowed to execute model.";
const char kExceptionMessageGenericError[] = "Other generic failures occurred.";
const char kExceptionMessageFiltered[] =
    "The execution yielded an unsafe response.";
const char kExceptionMessageOutputLanguageFiltered[] =
    "The model attempted to output text in an untested language, and was "
    "prevented from doing so.";
const char kExceptionMessageResponseLowQuality[] =
    "The model attempted to output text with low quality, and was prevented "
    "from doing so.";
const char kExceptionMessageDisabled[] = "The response was disabled.";
const char kExceptionMessageCancelled[] = "The request was cancelled.";
const char kExceptionMessageSessionDestroyed[] =
    "The model execution session has been destroyed.";
const char kExceptionMessageRequestAborted[] = "The request has been aborted.";
const char kExceptionMessageInputTooLarge[] = "The input is too large.";
const char kExceptionMessageResponseExceedsMaxTokens[] =
    "The response exceeded output limits and was truncated.";
const char kExceptionMessageResponseExceedsRemainingContext[] =
    "The response size exceeded the remaining available context.";
const char kExceptionMessageResponseParsingFailed[] =
    "Failed to parse the response.";
const char kExceptionMessageFailedToRunSafety[] =
    "Failed to run the safety checks.";
const char kExceptionMessageFailedToCountTokens[] = "Failed to count tokens.";
const char kExceptionMessageInvalidTemperatureAndTopKFormat[] =
    "Initializing a new session must either specify both topK and temperature, "
    "or neither of them.";
const char kExceptionMessageInvalidTopK[] =
    "The topK value provided is invalid.";
const char kExceptionMessageInvalidTemperature[] =
    "The temperature value provided is invalid.";
const char kExceptionMessageSamplingModeAndParamsConflict[] =
    "Cannot provide both 'samplingMode' and raw sampling parameters "
    "('temperature' or 'topK').";
const char kExceptionMessageUnableToCreateSession[] =
    "The device is unable to create a session to run the model. "
    "Please check the result of availability() first.";
const char kExceptionMessageUnableToCloneSession[] =
    "The session cannot be cloned.";
const char kExceptionMessageUnableToCalculateUsage[] =
    "The usage cannot be calculated.";
const char kExceptionMessagePromptWithSystemRoleIsNotTheFirst[] =
    "The 'system' role message must be the first message of a session.";
const char kExceptionMessageUnsupportedLanguages[] =
    "The specified languages are not supported.";
const char kExceptionMessageIncompatiblePreferenceOptions[] =
    "The specified options are not supported with the 'speed' performance "
    "preference.";
const char kExceptionMessageInvalidRequest[] =
    "The request is invalid - the input or options could not be processed.";
const char kExceptionMessageInvalidResponseJsonSchema[] =
    "Response constraint is not a supported json schema.";
const char kExceptionMessagePermissionPolicy[] =
    "Access denied because the Permission Policy is not enabled.";
const char kExceptionMessageUserActivationRequired[] =
    "Requires a user gesture when availability is \"downloading\" or "
    "\"downloadable\".";
const char kExceptionMessageSpeculativeDecodingSamplingConflict[] =
    "The sampling options are incompatible with speculative decoding (MTP). "
    "Prompt API sessions must specify compatible sampling options, i.e. "
    "`samplingMode:'most-predictable'` or `topK:1` or `temperature:0`.";
const char kExceptionMessageSpeculativeDecodingConstraintConflict[] =
    "Constrained decoding (responseConstraint) cannot be used with speculative "
    "decoding (MTP).";

void ThrowInvalidContextException(ExceptionState& exception_state) {
  exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                    kExceptionMessageExecutionContextInvalid);
}

void ThrowDocumentNotActiveException(ExceptionState& exception_state) {
  exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                    kExceptionMessageDocumentNotActive);
}

void ThrowSessionDestroyedException(ExceptionState& exception_state) {
  exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                    kExceptionMessageSessionDestroyed);
}

void ThrowAbortedException(ExceptionState& exception_state) {
  exception_state.ThrowDOMException(DOMExceptionCode::kAbortError,
                                    kExceptionMessageRequestAborted);
}

void RejectPromiseWithInternalError(ScriptPromiseResolverBase* resolver) {
  if (resolver) {
    resolver->Reject(CreateInternalErrorException());
  }
}

DOMException* CreateInternalErrorException() {
  return DOMException::Create(
      kExceptionMessageServiceUnavailable,
      DOMException::GetErrorName(DOMExceptionCode::kOperationError));
}

DOMException* CreateSessionDestroyedException() {
  return DOMException::Create(
      kExceptionMessageSessionDestroyed,
      DOMException::GetErrorName(DOMExceptionCode::kInvalidStateError));
}

bool HandleAbortSignal(AbortSignal* signal,
                       ScriptState* script_state,
                       ExceptionState& exception_state) {
  if (signal && signal->aborted()) {
    auto reason = signal->reason(script_state);
    if (reason.IsEmpty()) {
      ThrowAbortedException(exception_state);
    } else {
      V8ThrowException::ThrowException(script_state->GetIsolate(),
                                       reason.V8Value());
    }
    return true;
  }

  return false;
}

bool ValidateScriptState(ScriptState* script_state,
                         ExceptionState& exception_state,
                         bool permit_workers) {
  if (!script_state->ContextIsValid()) {
    ThrowInvalidContextException(exception_state);
    return false;
  }

  ExecutionContext* context = ExecutionContext::From(script_state);

  if (context->IsServiceWorkerGlobalScope()) {
    return permit_workers;
  }

  LocalDOMWindow* window = DynamicTo<LocalDOMWindow>(context);

  // Realm’s global object must be a Window object.
  CHECK(window);

  // If document is not fully active, then return a promise rejected with an
  // "InvalidStateError" DOMException.
  Document* document = window->document();
  CHECK(document);
  if (!document->IsActive()) {
    ThrowDocumentNotActiveException(exception_state);
    return false;
  }

  return true;
}

String ValidateAndStringifyObject(const ScriptValue& input,
                                  ScriptState* script_state,
                                  ExceptionState& exception_state) {
  v8::Local<v8::String> value;
  if (!input.V8Value()->IsObject() ||
      !v8::JSON::Stringify(script_state->GetContext(),
                           input.V8Value().As<v8::Object>())
           .ToLocal(&value)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        kExceptionMessageInvalidResponseJsonSchema);
    return String();
  }
  return ToBlinkString<String>(script_state->GetIsolate(), value,
                               kDoNotExternalize);
}

namespace {
// Create an UnknownError exception, include `error` in the exception
// message. This is intended for handling values of
// `ModelStreamingResponseStatus` that we do not expect to ever see when
// using an on-device model, e.g. errors related to servers.
DOMException* CreateUnknown(const char* error) {
  return DOMException::Create(
      StrCat({"An unknown error occurred: ", error}),
      DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
}
}  // namespace

DOMException* ConvertModelStreamingResponseErrorToDOMException(
    ModelStreamingResponseStatus error,
    mojom::blink::QuotaErrorInfoPtr quota_error_info) {
  switch (error) {
    case ModelStreamingResponseStatus::kErrorUnknown:
      base::debug::DumpWithoutCrashing();
      return CreateUnknown("kErrorUnknown");
    case ModelStreamingResponseStatus::kErrorInvalidRequest:
      base::debug::DumpWithoutCrashing();
      return DOMException::Create(
          kExceptionMessageInvalidRequest,
          DOMException::GetErrorName(DOMExceptionCode::kNotSupportedError));
    case ModelStreamingResponseStatus::kErrorRequestThrottled:
      base::debug::DumpWithoutCrashing();
      return CreateUnknown("kErrorRequestThrottled");
    case ModelStreamingResponseStatus::kErrorPermissionDenied:
      return DOMException::Create(
          kExceptionMessagePermissionDenied,
          DOMException::GetErrorName(DOMExceptionCode::kNotAllowedError));
    case ModelStreamingResponseStatus::kErrorGenericFailure:
      return DOMException::Create(
          kExceptionMessageGenericError,
          DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
    case ModelStreamingResponseStatus::kErrorRetryableError:
      base::debug::DumpWithoutCrashing();
      return CreateUnknown("kErrorRetryableError");
    case ModelStreamingResponseStatus::kErrorNonRetryableError:
      base::debug::DumpWithoutCrashing();
      return CreateUnknown("kErrorNonRetryableError");
    case ModelStreamingResponseStatus::kErrorUnsupportedLanguage:
      return DOMException::Create(
          kExceptionMessageOutputLanguageFiltered,
          DOMException::GetErrorName(DOMExceptionCode::kNotSupportedError));
    case ModelStreamingResponseStatus::kErrorFiltered:
      return DOMException::Create(
          kExceptionMessageFiltered,
          DOMException::GetErrorName(DOMExceptionCode::kNotReadableError));
    case ModelStreamingResponseStatus::kErrorDisabled:
      return DOMException::Create(
          kExceptionMessageDisabled,
          DOMException::GetErrorName(DOMExceptionCode::kAbortError));
    case ModelStreamingResponseStatus::kErrorCancelled:
      return DOMException::Create(
          kExceptionMessageCancelled,
          DOMException::GetErrorName(DOMExceptionCode::kAbortError));
    case ModelStreamingResponseStatus::kErrorSessionDestroyed:
      return DOMException::Create(
          kExceptionMessageSessionDestroyed,
          DOMException::GetErrorName(DOMExceptionCode::kInvalidStateError));
    case ModelStreamingResponseStatus::kErrorInputTooLarge:
      if (RuntimeEnabledFeatures::QuotaExceededErrorUpdateEnabled()) {
        CHECK(quota_error_info);
        auto* options = MakeGarbageCollected<QuotaExceededErrorOptions>();
        options->setQuota(static_cast<double>(quota_error_info->quota));
        options->setRequested(static_cast<double>(quota_error_info->requested));
        return QuotaExceededError::Create(kExceptionMessageInputTooLarge,
                                          std::move(options));
      }
      return DOMException::Create(
          kExceptionMessageInputTooLarge,
          DOMException::GetErrorName(DOMExceptionCode::kQuotaExceededError));
    case ModelStreamingResponseStatus::kErrorResponseLowQuality:
      return DOMException::Create(
          kExceptionMessageResponseLowQuality,
          DOMException::GetErrorName(DOMExceptionCode::kNotSupportedError));
    case ModelStreamingResponseStatus::kErrorResponseExceedsMaxTokens:
      return DOMException::Create(
          kExceptionMessageResponseExceedsMaxTokens,
          DOMException::GetErrorName(DOMExceptionCode::kQuotaExceededError));
    case ModelStreamingResponseStatus::kErrorResponseExceedsRemainingContext:
      return DOMException::Create(
          kExceptionMessageResponseExceedsRemainingContext,
          DOMException::GetErrorName(DOMExceptionCode::kQuotaExceededError));
    case ModelStreamingResponseStatus::kErrorResponseParsingFailed:
      return DOMException::Create(
          kExceptionMessageResponseParsingFailed,
          DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
    case ModelStreamingResponseStatus::kErrorFailedToRunSafety:
      return DOMException::Create(
          kExceptionMessageFailedToRunSafety,
          DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
    case ModelStreamingResponseStatus::kErrorFailedToCountTokens:
      return DOMException::Create(
          kExceptionMessageFailedToCountTokens,
          DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
    case ModelStreamingResponseStatus::kOngoing:
    case ModelStreamingResponseStatus::kComplete:
      NOTREACHED();
  }
  NOTREACHED();
}

// LINT.IfChange(ConvertModelAvailabilityCheckResultToDebugString)
String ConvertModelAvailabilityCheckResultToDebugString(
    mojom::blink::ModelAvailabilityCheckResult result) {
  switch (result) {
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableServiceNotRunning:
      return "Unable to create a text session because the service is not "
             "running.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableUnsupportedLanguage:
      return "The requested language options are not supported.";
    case mojom::blink::ModelAvailabilityCheckResult::kUnavailableUnknown:
      return "The service is unable to create new session.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableFeatureNotEnabled:
      return "The feature flag gating model execution was disabled.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableConfigNotAvailableForFeature:
      return "The model was available but there was not an execution config "
             "available for the feature.";
    case mojom::blink::ModelAvailabilityCheckResult::kUnavailableGpuBlocked:
      return "The GPU is blocked.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableTooManyRecentCrashes:
      return "The model process crashed too many times for this version. Check "
             "chrome://crashes for additional information.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableSafetyModelNotAvailable:
      return "The safety model was required but not available.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableSafetyConfigNotAvailableForFeature:
      return "The safety model was available but there was not a safety config "
             "available for the feature.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableLanguageDetectionModelNotAvailable:
      return "The language detection model was required but not available.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableFeatureExecutionNotEnabled:
      return "Model execution for this feature was not enabled.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableModelAdaptationNotAvailable:
      return "Model capability is not available.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableValidationPending:
      return "Model validation is still pending.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableValidationFailed:
      return "Model validation failed.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableModelNotEligible:
      return "The device is not eligible for running on-device model.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableInsufficientDiskSpace:
      return "The device does not have enough space for downloading the "
             "on-device model.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableInsufficientDiskSpaceForCaches:
      return "The device does not have enough disk space to initialize "
             "the on-device model.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableTranslationNotEligible:
      return "The on-device translation is not available.";
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableEnterprisePolicyDisabled:
      return "The on-device model is not available because the enterprise "
             "policy disables the feature.";

    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableIncompatiblePreferenceOptions:
      return kExceptionMessageIncompatiblePreferenceOptions;
    case mojom::blink::ModelAvailabilityCheckResult::
        kUnavailableIncompatibleSpeculativeDecodingOptions:
      return kExceptionMessageSpeculativeDecodingSamplingConflict;
    case mojom::blink::ModelAvailabilityCheckResult::kAvailable:
    case mojom::blink::ModelAvailabilityCheckResult::kDownloadable:
    case mojom::blink::ModelAvailabilityCheckResult::kDownloading:
      NOTREACHED();
  }
  NOTREACHED();
}
// LINT.ThenChange(//third_party/blink/public/mojom/ai/ai_manager.mojom:ModelAvailabilityCheckResult)

}  // namespace blink
