// 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 "services/device/geolocation/network_location_request.h"

#include <stdint.h>

#include <algorithm>
#include <iterator>
#include <limits>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/device_event_log/device_event_log.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/device/public/cpp/device_features.h"
#include "services/device/public/cpp/geolocation/geoposition.h"
#include "services/device/public/mojom/geolocation_internals.mojom.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 "services/network/public/mojom/url_response_head.mojom.h"

namespace device {
namespace {

#if BUILDFLAG(ARKWEB_PRIVACY_COMPLIANCE)
const char kNetworkLocationBaseUrl[] =
    "https://x.x.x.x";
#else
const char kNetworkLocationBaseUrl[] =
    "https://www.googleapis.com/geolocation/v1/geolocate";
#endif

const char kLocationString[] = "location";
const char kLatitudeString[] = "lat";
const char kLongitudeString[] = "lng";
const char kAccuracyString[] = "accuracy";

// Keys for the network request.
constexpr std::string_view kAgeKey = "age";
constexpr std::string_view kChannelKey = "channel";
constexpr std::string_view kMacAddressKey = "macAddress";
constexpr std::string_view kSignalStrengthKey = "signalStrength";
constexpr std::string_view kSignalToNoiseRatioKey = "signalToNoiseRatio";
constexpr std::string_view kWifiAccessPointsKey = "wifiAccessPoints";

void RecordUmaLocationRequestResult(NetworkLocationRequestResult result_code) {
  UMA_HISTOGRAM_ENUMERATION("Geolocation.NetworkLocationRequest.Result",
                            result_code,
                            NetworkLocationRequestResult::kMaxValue);
}

void RecordUmaResponseCode(int code) {
  base::UmaHistogramSparse("Geolocation.NetworkLocationRequest.ResponseCode",
                           code);
}

void RecordUmaRequestInterval(base::TimeDelta time_delta) {
  const int kMin = 1;
  const int kMax = 11;
  const int kBuckets = 10;
  UMA_HISTOGRAM_CUSTOM_COUNTS(
      "Geolocation.NetworkLocationRequest.RequestInterval",
      time_delta.InMinutes(), kMin, kMax, kBuckets);
}

void RecordUmaNetworkLocationRequestSource(
    NetworkLocationRequestSource network_location_request_source) {
  base::UmaHistogramEnumeration("Geolocation.NetworkLocationRequest.Source",
                                network_location_request_source);
}

void RecordUmaAccuracy(int accuracy) {
  base::UmaHistogramCounts1M("Geolocation.NetworkLocationRequest.Accuracy",
                             accuracy);
}

// Local functions

// Returns a URL for a request to the Google Maps geolocation API. If the
// specified |api_key| is not empty, it is escaped and included as a query
// string parameter.
GURL FormRequestURL(const std::string& api_key);

base::Value::Dict FormUploadData(const WifiData& wifi_data,
                                 const base::Time& wifi_timestamp);

// Attempts to create `LocationResponseResult` from the network request
// response. Detects and indicates various failure cases.
LocationResponseResult CreateResultFromResponse(
    const base::Value::Dict& response_body,
    const base::Time& wifi_timestamp,
    const GURL& server_url);

mojom::GeopositionResultPtr CreateGeopositionErrorResult(
    const GURL& server_url,
    const std::string& error_message,
    const std::string& error_technical);

// Returns a `mojom::GeopositionPtr` containing the position estimate in
// `response_body`, or `nullptr` if no valid fix was received. The timestamp
// for the returned estimate is set to `wifi_timestamp`.
mojom::GeopositionPtr CreateGeoposition(const base::Value::Dict& response_body,
                                        const base::Time& wifi_timestamp);
void AddWifiData(const WifiData& wifi_data,
                 int age_milliseconds,
                 base::Value::Dict& request);

std::vector<mojom::AccessPointDataPtr> RequestToMojom(
    const base::Value::Dict& request_dict,
    const base::Time& wifi_timestamp) {
  const auto* access_points_list = request_dict.FindList(kWifiAccessPointsKey);
  if (!access_points_list) {
    return {};
  }
  std::vector<mojom::AccessPointDataPtr> request;
  std::ranges::transform(
      *access_points_list, std::back_inserter(request),
      [&wifi_timestamp](const base::Value& ap_value) {
        const auto& ap_dict = ap_value.GetDict();
        // kMacAddressKey is required, all other keys are optional.
        const auto* mac_address = ap_dict.FindString(kMacAddressKey);
        CHECK(mac_address);
        auto result = mojom::AccessPointData::New();
        result->mac_address = *mac_address;
        if (auto age = ap_dict.FindInt(kAgeKey)) {
          result->timestamp = wifi_timestamp - base::Milliseconds(*age);
        }
        if (auto signal_strength = ap_dict.FindInt(kSignalStrengthKey)) {
          result->radio_signal_strength = *signal_strength;
        }
        if (auto channel = ap_dict.FindInt(kChannelKey)) {
          result->channel = *channel;
        }
        if (auto snr = ap_dict.FindInt(kSignalToNoiseRatioKey)) {
          result->signal_to_noise = *snr;
        }
        return result;
      });
  return request;
}

mojom::NetworkLocationResponsePtr ResponseToMojom(
    const base::Value::Dict& response_dict) {
  const auto* location_dict = response_dict.FindDict(kLocationString);
  if (location_dict) {
    auto latitude = location_dict->FindDouble(kLatitudeString);
    auto longitude = location_dict->FindDouble(kLongitudeString);
    if (latitude && longitude) {
      return mojom::NetworkLocationResponse::New(
          *latitude, *longitude, response_dict.FindDouble(kAccuracyString));
    }
  }
  return nullptr;
}

}  // namespace

LocationResponseResult::LocationResponseResult(
    mojom::GeopositionResultPtr position,
    NetworkLocationRequestResult result_code,
    mojom::NetworkLocationResponsePtr raw_response)
    : position(std::move(position)),
      result_code(result_code),
      raw_response(std::move(raw_response)) {}

LocationResponseResult::LocationResponseResult(LocationResponseResult&& other) =
    default;
LocationResponseResult& LocationResponseResult::operator=(
    LocationResponseResult&& other) = default;

LocationResponseResult::~LocationResponseResult() = default;

NetworkLocationRequest::NetworkLocationRequest(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    const std::string& api_key,
    LocationResponseCallback callback)
    : url_loader_factory_(std::move(url_loader_factory)),
      api_key_(api_key),
      location_response_callback_(std::move(callback)) {}

NetworkLocationRequest::~NetworkLocationRequest() = default;

void NetworkLocationRequest::MakeRequest(
    const WifiData& wifi_data,
    const base::Time& wifi_timestamp,
    const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation,
    NetworkLocationRequestSource network_location_request_source) {
  GEOLOCATION_LOG(DEBUG)
      << "Sending a network location request: Number of Wi-Fi APs="
      << wifi_data.access_point_data.size();
  if (url_loader_) {
    GEOLOCATION_LOG(DEBUG) << "Cancelling pending network location request";
    DVLOG(1) << "NetworkLocationRequest : Cancelling pending request";
    RecordUmaLocationRequestResult(NetworkLocationRequestResult::kCanceled);
    url_loader_.reset();
  }
  wifi_data_ = wifi_data;

  if (!wifi_timestamp_.is_null()) {
    RecordUmaRequestInterval(wifi_timestamp - wifi_timestamp_);
  }
  wifi_timestamp_ = wifi_timestamp;

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::CompleteNetworkTrafficAnnotation("network_location_request",
                                            partial_traffic_annotation,
                                            R"(
        semantics {
          description:
            "Obtains geo position based on current IP address and local "
            "network information including Wi-Fi access points (even if you’re "
            "not using them)."
          trigger:
            "Location requests are sent when the page requests them or new "
            "IP address is available."
          data: "Wi-Fi data, IP address."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: NO
      })");

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->method = "POST";
  resource_request->url = FormRequestURL(api_key_);
  DCHECK(resource_request->url.is_valid());
  resource_request->load_flags =
      net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                 traffic_annotation);
  url_loader_->SetAllowHttpErrorResults(true);

  request_data_ = FormUploadData(wifi_data, wifi_timestamp);
  std::string upload_data = base::WriteJson(request_data_).value_or("");
  url_loader_->AttachStringForUpload(upload_data, "application/json");

  url_loader_->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&NetworkLocationRequest::OnRequestComplete,
                     base::Unretained(this)),
      1024 * 1024 /* 1 MiB */);
  RecordUmaNetworkLocationRequestSource(network_location_request_source);
}

void NetworkLocationRequest::OnRequestComplete(
    std::optional<std::string> data) {
  int response_code = 0;
  if (url_loader_->ResponseInfo())
    response_code = url_loader_->ResponseInfo()->headers->response_code();
  RecordUmaResponseCode(response_code);
  GEOLOCATION_LOG(DEBUG) << "Got network location response: response_code="
                         << response_code;

  LocationResponseResult result{mojom::GeopositionResultPtr(),
                                NetworkLocationRequestResult::kSuccess,
                                mojom::NetworkLocationResponsePtr()};
  // HttpPost can fail for a number of reasons. Most likely this is because
  // we're offline, or there was no response.
  const int net_error = url_loader_->NetError();
  if (net_error != net::OK) {
    result.position =
        CreateGeopositionErrorResult(url_loader_->GetFinalURL(),
                                     "Network error. Check "
                                     "DevTools console for more information.",
                                     net::ErrorToShortString(net_error));
    result.result_code = NetworkLocationRequestResult::kNetworkError;
  } else if (response_code != net::HTTP_OK) {
    result.position = CreateGeopositionErrorResult(
        url_loader_->GetFinalURL(),
        "Failed to query location from network service. Check "
        "the DevTools console for more information.",
        base::StringPrintf("Returned error code %d", response_code));
    result.result_code = NetworkLocationRequestResult::kResponseNotOk;
  } else if (!data.has_value()) {
    result.position = CreateGeopositionErrorResult(
        url_loader_->GetFinalURL(), "Network request response body is empty.",
        "");
    result.result_code = NetworkLocationRequestResult::kResponseEmpty;
  } else {
    DVLOG(1) << "NetworkLocationRequest::OnRequestComplete() : "
                "Parsing response "
             << *data;
    auto response_result = base::JSONReader::ReadAndReturnValueWithError(
        *data, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
    if (!response_result.has_value()) {
      LOG(WARNING) << "NetworkLocationRequest::OnRequestComplete() : "
                      "JSONReader failed : "
                   << response_result.error().message;
    } else if (!response_result->is_dict()) {
      LOG(WARNING) << "NetworkLocationRequest::OnRequestComplete() : "
                      "Unexpected response type "
                   << response_result->type();
    } else {
      base::Value::Dict response_data = std::move(*response_result).TakeDict();
      result = CreateResultFromResponse(response_data, wifi_timestamp_,
                                        url_loader_->GetFinalURL());
    }
    if (!result.position) {
      // We failed to parse the response.
      result.position = CreateGeopositionErrorResult(url_loader_->GetFinalURL(),
                                                     "Response was malformed",
                                                     /*error_technical=*/"");
      result.result_code = NetworkLocationRequestResult::kResponseMalformed;
    }
  }

  url_loader_.reset();

  DVLOG(1) << "NetworkLocationRequest::OnRequestComplete() : run callback.";
  RecordUmaLocationRequestResult(result.result_code);
  location_response_callback_.Run(std::move(result), wifi_data_);
}

std::vector<mojom::AccessPointDataPtr>
NetworkLocationRequest::GetRequestDataForDiagnostics() const {
  return RequestToMojom(request_data_, wifi_timestamp_);
}

// Local functions.
namespace {

struct AccessPointLess {
  bool operator()(const mojom::AccessPointData* ap1,
                  const mojom::AccessPointData* ap2) const {
    return ap2->radio_signal_strength < ap1->radio_signal_strength;
  }
};

GURL FormRequestURL(const std::string& api_key) {
  GURL url(kNetworkLocationBaseUrl);
  if (!api_key.empty()) {
    std::string query(url.GetQuery());
    if (!query.empty())
      query += "&";
    query += "key=" + base::EscapeQueryParamValue(api_key, true);
    GURL::Replacements replacements;
    replacements.SetQueryStr(query);
    return url.ReplaceComponents(replacements);
  }
  return url;
}

base::Value::Dict FormUploadData(const WifiData& wifi_data,
                                 const base::Time& wifi_timestamp) {
  int age = std::numeric_limits<int32_t>::min();  // Invalid so AddInteger()
                                                  // will ignore.
  if (!wifi_timestamp.is_null()) {
    // Convert absolute timestamps into a relative age.
    int64_t delta_ms = (base::Time::Now() - wifi_timestamp).InMilliseconds();
    if (delta_ms >= 0 && delta_ms < std::numeric_limits<int32_t>::max())
      age = static_cast<int>(delta_ms);
  }

  base::Value::Dict request;
  AddWifiData(wifi_data, age, request);
  return request;
}

void AddString(std::string_view property_name,
               const std::string& value,
               base::Value::Dict& dict) {
  if (!value.empty())
    dict.Set(property_name, value);
}

void AddInteger(std::string_view property_name,
                int value,
                base::Value::Dict& dict) {
  if (value != std::numeric_limits<int32_t>::min())
    dict.Set(property_name, value);
}

void AddWifiData(const WifiData& wifi_data,
                 int age_milliseconds,
                 base::Value::Dict& request) {
  if (wifi_data.access_point_data.empty())
    return;

  typedef std::multiset<const mojom::AccessPointData*, AccessPointLess>
      AccessPointSet;
  AccessPointSet access_points_by_signal_strength;

  for (const auto& ap_data : wifi_data.access_point_data)
    access_points_by_signal_strength.insert(&ap_data);

  base::Value::List wifi_access_point_list;
  for (auto* ap_data : access_points_by_signal_strength) {
    if (ap_data->mac_address.empty()) {
      continue;
    }
    base::Value::Dict wifi_dict;
    AddString(kMacAddressKey, ap_data->mac_address, wifi_dict);
    AddInteger(kSignalStrengthKey, ap_data->radio_signal_strength, wifi_dict);
    AddInteger(kAgeKey, age_milliseconds, wifi_dict);
    AddInteger(kChannelKey, ap_data->channel, wifi_dict);
    AddInteger(kSignalToNoiseRatioKey, ap_data->signal_to_noise, wifi_dict);
    wifi_access_point_list.Append(std::move(wifi_dict));
  }
  if (!wifi_access_point_list.empty())
    request.Set(kWifiAccessPointsKey, std::move(wifi_access_point_list));
}

mojom::GeopositionResultPtr CreateGeopositionErrorResult(
    const GURL& server_url,
    const std::string& error_message,
    const std::string& error_technical) {
  auto error = mojom::GeopositionError::New();
  error->error_code = mojom::GeopositionErrorCode::kPositionUnavailable;
  error->error_message = error_message;
  VLOG(1) << "NetworkLocationRequest::CreateGeopositionErrorResult() : "
          << error->error_message;
  if (!error_technical.empty()) {
    error->error_technical = "Network location provider at '";
    error->error_technical += server_url.DeprecatedGetOriginAsURL().spec();
    error->error_technical += "' : ";
    error->error_technical += error_technical;
    error->error_technical += ".";
    VLOG(1) << "NetworkLocationRequest::CreateGeopositionErrorResult() : "
            << error->error_technical;
  }
  return mojom::GeopositionResult::NewError(std::move(error));
}

LocationResponseResult CreateResultFromResponse(
    const base::Value::Dict& response_body,
    const base::Time& wifi_timestamp,
    const GURL& server_url) {
  // We use the timestamp from the wifi data that was used to generate
  // this position fix.
  mojom::GeopositionPtr position =
      CreateGeoposition(response_body, wifi_timestamp);
  auto response = ResponseToMojom(response_body);
  if (!position) {
    // We failed to parse the response.
    return LocationResponseResult(
        CreateGeopositionErrorResult(server_url, "Response was malformed",
                                     /*error_technical=*/""),
        NetworkLocationRequestResult::kResponseMalformed, std::move(response));
  }

  // The response was successfully parsed, but it may not be a valid
  // position fix.
  if (!ValidateGeoposition(*position)) {
    return LocationResponseResult(
        CreateGeopositionErrorResult(server_url,
                                     "Did not provide a good position fix",
                                     /*error_technical=*/""),
        NetworkLocationRequestResult::kInvalidPosition, std::move(response));
  }

  return LocationResponseResult(
      mojom::GeopositionResult::NewPosition(std::move(position)),
      NetworkLocationRequestResult::kSuccess, std::move(response));
}

mojom::GeopositionPtr CreateGeoposition(const base::Value::Dict& response_body,
                                        const base::Time& wifi_timestamp) {
  DCHECK(!wifi_timestamp.is_null());

  if (response_body.empty()) {
    LOG(WARNING) << "CreateGeoposition() : Response was empty.";
    return nullptr;
  }

  // Get the location
  const base::Value* location_value = response_body.Find(kLocationString);
  if (!location_value) {
    VLOG(1) << "CreateGeoposition() : Missing location attribute.";
    // GLS returns a response with no location property to represent
    // no fix available; return an invalid geoposition to indicate successful
    // parse.
    // TODO(mattreynolds): Return an appropriate error instead of a
    // default-initialized Geoposition.
    return mojom::Geoposition::New();
  }

  const base::Value::Dict* location_object = location_value->GetIfDict();
  if (!location_object) {
    if (!location_value->is_none()) {
      VLOG(1) << "CreateGeoposition() : Unexpected location type "
              << location_value->type();
      // If the network provider was unable to provide a position fix, it should
      // return a HTTP 200, with "location" : null. Otherwise it's an error.
      return nullptr;
    }
    // Successfully parsed response containing no fix.
    // TODO(mattreynolds): Return an appropriate error instead of a
    // default-initialized Geoposition.
    return mojom::Geoposition::New();
  }

  // latitude and longitude fields are always required.
  std::optional<double> latitude = location_object->FindDouble(kLatitudeString);
  std::optional<double> longitude =
      location_object->FindDouble(kLongitudeString);
  if (!latitude || !longitude) {
    VLOG(1) << "CreateGeoposition() : location lacks lat and/or long.";
    return nullptr;
  }
  // All error paths covered.
  auto position = mojom::Geoposition::New();
  position->latitude = *latitude;
  position->longitude = *longitude;
  position->timestamp = wifi_timestamp;

  // Other fields are optional.
  std::optional<double> accuracy = response_body.FindDouble(kAccuracyString);
  if (accuracy) {
    position->accuracy = *accuracy;
    RecordUmaAccuracy(static_cast<int>(*accuracy));
  }

  return position;
}

}  // namespace

}  // namespace device