// 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 "components/manta/manta_service_callbacks.h"

#include <memory>
#include <string>
#include <string_view>

#include "base/containers/fixed_flat_map.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "components/endpoint_fetcher/endpoint_fetcher.h"
#include "components/manta/manta_status.h"
#include "components/manta/proto/manta.pb.h"
#include "components/manta/proto/rpc_status.pb.h"
#include "net/http/http_status_code.h"

namespace manta {

namespace {
constexpr char kTypeUrlRpcErrorInfo[] =
    "type.googleapis.com/google.rpc.ErrorInfo";
constexpr char kTypeUrlRpcLocalizedMessage[] =
    "type.googleapis.com/google.rpc.LocalizedMessage";
constexpr char kExpectedEndPointDomain[] = "aratea-pa.googleapis.com";

// Maps the RpcErrorInfo.reason to MantaStatusCode.
std::optional<MantaStatusCode> MapServerFailureReasonToMantaStatusCode(
    const std::string& reason) {
  static constexpr auto reason_map =
      base::MakeFixedFlatMap<std::string_view, MantaStatusCode>({
          {"MISSING_INPUT", MantaStatusCode::kInvalidInput},
          {"INVALID_INPUT", MantaStatusCode::kInvalidInput},
          {"UNSUPPORTED_LANGUAGE", MantaStatusCode::kUnsupportedLanguage},
          {"RESTRICTED_COUNTRY", MantaStatusCode::kRestrictedCountry},
          {"RESOURCE_EXHAUSTED", MantaStatusCode::kResourceExhausted},
          {"PER_USER_QUOTA_EXCEEDED", MantaStatusCode::kPerUserQuotaExceeded},
      });
  const auto iter = reason_map.find(reason);

  return iter != reason_map.end() ? std::optional<MantaStatusCode>(iter->second)
                                  : std::nullopt;
}

// Maps the RpcStatus.code to MantaStatusCode.
// The RpcStatus.code is an enum value of google.rpc.Code.
std::optional<MantaStatusCode> MapServerStatusCodeToMantaStatusCode(
    const int32_t server_status_code) {
  // TODO(b/288019728): add more items when needed.
  static constexpr auto code_map =
      base::MakeFixedFlatMap<int32_t, MantaStatusCode>({
          {3 /*INVALID_ARGUMENT*/, MantaStatusCode::kInvalidInput},
          {8 /*RESOURCE_EXHAUSTED*/, MantaStatusCode::kResourceExhausted},
      });
  const auto iter = code_map.find(server_status_code);

  return iter != code_map.end() ? std::optional<MantaStatusCode>(iter->second)
                                : std::nullopt;
}

void LogTimeCost(const MantaMetricType request_type,
                 base::TimeDelta time_cost) {
  switch (request_type) {
    case MantaMetricType::kOrca:
      base::UmaHistogramTimes("Ash.MantaService.OrcaProvider.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kScanner:
      base::UmaHistogramTimes("Ash.MantaService.ScannerProvider.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kSnapper:
      base::UmaHistogramTimes("Ash.MantaService.SnapperProvider.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kMahiSummary:
      base::UmaHistogramTimes("Ash.MantaService.MahiProvider.Summary.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kMahiElucidation:
      base::UmaHistogramTimes(
          "Ash.MantaService.MahiProvider.Elucidation.TimeCost", time_cost);
      break;
    case MantaMetricType::kMahiQA:
      base::UmaHistogramTimes("Ash.MantaService.MahiProvider.QA.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kAnchovy:
      base::UmaHistogramTimes("Ash.MantaService.AnchovyProvider.TimeCost",
                              time_cost);
      break;
    case MantaMetricType::kWalrus:
      base::UmaHistogramTimes("Ash.MantaService.WalrusProvider.TimeCost",
                              time_cost);
      break;
  }
}

void LogMantaStatusCode(const MantaMetricType request_type,
                        const MantaStatusCode status_code) {
  switch (request_type) {
    case MantaMetricType::kOrca:
      base::UmaHistogramEnumeration("Ash.MantaService.OrcaProvider.StatusCode",
                                    status_code);
      break;
    case MantaMetricType::kScanner:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.ScannerProvider.StatusCode", status_code);
      break;
    case MantaMetricType::kSnapper:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.SnapperProvider.StatusCode", status_code);
      break;
    case MantaMetricType::kMahiSummary:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.MahiProvider.Summary.StatusCode", status_code);
      break;
    case MantaMetricType::kMahiElucidation:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.MahiProvider.Elucidation.StatusCode", status_code);
      break;
    case MantaMetricType::kMahiQA:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.MahiProvider.QA.StatusCode", status_code);
      break;
    case MantaMetricType::kAnchovy:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.AnchovyProvider.StatusCode", status_code);
      break;
    case MantaMetricType::kWalrus:
      base::UmaHistogramEnumeration(
          "Ash.MantaService.WalrusProvider.StatusCode", status_code);
      break;
  }
}
}  // namespace

void OnEndpointFetcherComplete(
    MantaProtoResponseCallback callback,
    base::Time start_time,
    const MantaMetricType request_type,
    std::unique_ptr<endpoint_fetcher::EndpointFetcher> fetcher,
    std::unique_ptr<endpoint_fetcher::EndpointResponse> responses) {
  // Tries to parse the response as a Response proto and return to the
  // `callback` together with a OK status, or capture the errors and return a
  // proper error status.

  base::TimeDelta time_cost = base::Time::Now() - start_time;

  std::string message, locale;

  if (!responses) {
    LogMantaStatusCode(request_type, MantaStatusCode::kBackendFailure);
    std::move(callback).Run(nullptr,
                            {MantaStatusCode::kBackendFailure, message});
    return;
  }

  // Tries to parse the unexpected response as RpcStatus and passes back the
  // error messages.
  if (responses->error_type.has_value() ||
      responses->http_status_code != net::HTTP_OK) {
    MantaStatusCode manta_status_code = MantaStatusCode::kBackendFailure;

    if (responses->error_type.has_value() &&
        responses->error_type.value() ==
            endpoint_fetcher::FetchErrorType::kNetError) {
      manta_status_code = MantaStatusCode::kNoInternetConnection;
    }

    proto::RpcStatus rpc_status;

    if (!rpc_status.ParseFromString(responses->response)) {
      DVLOG(1) << "Got unexpected failed response but failed to parse a "
                  "RpcStatus proto";
      LogMantaStatusCode(request_type, manta_status_code);
      std::move(callback).Run(nullptr, {manta_status_code, message});
      return;
    }

    // Tries to map RpcStatus.code to a more specific manta status code.
    auto maybe_updated_status_code =
        MapServerStatusCodeToMantaStatusCode(rpc_status.code());
    if (maybe_updated_status_code != std::nullopt) {
      manta_status_code = maybe_updated_status_code.value();
    }

    // Extracts clearer error code and user-friendly messages from details.
    for (const proto::Proto3Any& detail : rpc_status.details()) {
      if (detail.type_url() == kTypeUrlRpcErrorInfo) {
        proto::RpcErrorInfo error_info;
        error_info.ParseFromString(detail.value());

        // Tries to map ErrorInfo.reason to a more specific manta status code.
        maybe_updated_status_code =
            MapServerFailureReasonToMantaStatusCode(error_info.reason());
        if (error_info.domain() == kExpectedEndPointDomain &&
            maybe_updated_status_code != std::nullopt) {
          manta_status_code = maybe_updated_status_code.value();
        }
      } else if (detail.type_url() == kTypeUrlRpcLocalizedMessage) {
        proto::RpcLocalizedMessage localize_message;
        localize_message.ParseFromString(detail.value());
        message = localize_message.message();
        locale = localize_message.locale();
      }
    }

    LogMantaStatusCode(request_type, manta_status_code);
    std::move(callback).Run(nullptr, {manta_status_code, message, locale});

    return;
  }

  auto manta_response = std::make_unique<proto::Response>();
  if (!manta_response->ParseFromString(responses->response)) {
    DVLOG(1) << "Failed to parse MantaResponse as a Response proto";
    LogMantaStatusCode(request_type, MantaStatusCode::kMalformedResponse);
    std::move(callback).Run(nullptr,
                            {MantaStatusCode::kMalformedResponse, message});
    return;
  }

  LogTimeCost(request_type, time_cost);
  LogMantaStatusCode(request_type, MantaStatusCode::kOk);
  std::move(callback).Run(std::move(manta_response),
                          {MantaStatusCode::kOk, message});
}

}  // namespace manta
