// Copyright 2018 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/update_client/protocol_parser_json.h"

#include <algorithm>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/check.h"
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "base/version.h"
#include "components/update_client/protocol_definition.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"

namespace update_client {

namespace {

std::string GetValueString(const base::DictValue& node, const char* key) {
  const std::string* value = node.FindString(key);
  return value ? *value : std::string();
}

base::expected<std::string, std::string> Parse(const base::DictValue& node,
                                               const std::string& key) {
  const std::string* value = node.FindString(key);
  if (!value) {
    return base::unexpected(base::StrCat({"Missing ", key}));
  }
  return base::expected<std::string, std::string>(*value);
}

base::expected<base::Version, std::string> ParseVersion(
    const base::DictValue& node,
    const std::string& key) {
  base::expected<std::string, std::string> value = Parse(node, key);
  if (!value.has_value()) {
    return base::unexpected(value.error());
  }
  base::Version version(value.value());
  if (!version.IsValid()) {
    return base::unexpected(
        base::StrCat({"Invalid version: '", value.value(), "'."}));
  }
  return version;
}

std::optional<std::string> ParseOptional(const base::DictValue& node,
                                         const std::string& key) {
  const std::string* value = node.FindString(key);
  if (value) {
    return *value;
  }
  return std::nullopt;
}

int64_t ParseNumberWithDefault(const base::DictValue& node,
                               const std::string& key,
                               int64_t def) {
  const std::optional<double> value = node.FindDouble(key);
  if (value) {
    const double val = value.value();
    if (0 <= val && val < protocol_request::kProtocolMaxInt) {
      return static_cast<int64_t>(val);
    }
  }
  return def;
}

std::string ParseWithDefault(const base::DictValue& node,
                             const std::string& key,
                             const std::string& def) {
  const std::string* value = node.FindString(key);
  if (value) {
    return *value;
  }
  return def;
}

std::string ParseWithDefault(const base::DictValue& node,
                             const std::string& outer_key,
                             const std::string& inner_key,
                             const std::string& def) {
  const base::DictValue* outer = node.FindDict(outer_key);
  return outer ? ParseWithDefault(*outer, inner_key, def) : def;
}

base::expected<ProtocolParser::Operation, std::string> ParseOperation(
    const base::Value& node_val) {
  if (!node_val.is_dict()) {
    return base::unexpected("'operation' contains a non-dictionary.");
  }
  const base::DictValue& node = node_val.GetDict();
  ProtocolParser::Operation op;
  base::expected<std::string, std::string> type = Parse(node, "type");
  if (!type.has_value()) {
    return base::unexpected(type.error());
  }
  op.type = type.value();
  op.sha256_out = ParseWithDefault(node, "out", "sha256", {});
  op.sha256_in = ParseWithDefault(node, "in", "sha256", {});
  op.sha256_previous = ParseWithDefault(node, "previous", "sha256", {});
  op.path = ParseWithDefault(node, "path", {});
  op.arguments = ParseWithDefault(node, "arguments", {});
  op.size = ParseNumberWithDefault(node, "size", 0);
  if (const base::ListValue* list = node.FindList("urls")) {
    for (const base::Value& url_node : *list) {
      if (!url_node.is_dict()) {
        return base::unexpected("url node is not a dict");
      }
      base::expected<std::string, std::string> url =
          Parse(url_node.GetDict(), "url");
      if (!url.has_value()) {
        return base::unexpected(url.error());
      }
      GURL gurl(url.value());
      if (!gurl.is_valid()) {
        return base::unexpected("operation contains a malformed url");
      }
      op.urls.push_back(gurl);
    }
  }
  return op;
}

base::expected<ProtocolParser::Pipeline, std::string> ParsePipeline(
    const base::Value& node_val) {
  if (!node_val.is_dict()) {
    return base::unexpected("'pipeline' contains a non-dictionary.");
  }
  ProtocolParser::Pipeline pipeline;
  pipeline.pipeline_id =
      ParseWithDefault(node_val.GetDict(), "pipeline_id", {});
  if (const base::ListValue* node = node_val.GetDict().FindList("operations")) {
    for (const base::Value& operation_node : *node) {
      base::expected<ProtocolParser::Operation, std::string> operation =
          ParseOperation(operation_node);
      if (!operation.has_value()) {
        return base::unexpected(operation.error());
      }
      pipeline.operations.push_back(operation.value());
    }
  }
  return pipeline;
}

void ParseData(const base::Value& data_node_val, ProtocolParser::App* result) {
  if (!data_node_val.is_dict()) {
    return;
  }
  const base::DictValue& data_node = data_node_val.GetDict();

  result->data.emplace_back(GetValueString(data_node, "index"),
                            GetValueString(data_node, "#text"));
}

bool ParseUpdateCheck(const base::Value* node_val,
                      ProtocolParser::App* result,
                      std::string* error) {
  if (!node_val || !node_val->is_dict()) {
    *error = "'updatecheck' node is missing or not a dictionary.";
    return false;
  }
  const base::DictValue& node = node_val->GetDict();

  for (auto [k, v] : node) {
    if (!k.empty() && k.front() == '_' && v.is_string()) {
      result->custom_attributes[k] = v.GetString();
    }
  }

  // result->status was set to "ok" when parsing the app node; overwrite it with
  // the updatecheck status.
  base::expected<std::string, std::string> status = Parse(node, "status");
  if (!status.has_value()) {
    *error = status.error();
    return false;
  }
  result->status = status.value();

  if (result->status == "noupdate") {
    return true;
  }

  if (result->status == "ok") {
    base::expected<base::Version, std::string> nextversion =
        ParseVersion(node, "nextversion");
    if (nextversion.has_value()) {
      result->nextversion = nextversion.value();
    } else {
      *error = nextversion.error();
      return false;
    }

    if (const base::ListValue* list = node.FindList("pipelines")) {
      for (const base::Value& pipeline_node : *list) {
        base::expected<ProtocolParser::Pipeline, std::string> pipeline =
            ParsePipeline(pipeline_node);
        if (!pipeline.has_value()) {
          *error = pipeline.error();
          return false;
        }
        result->pipelines.push_back(pipeline.value());
      }
    }
    return true;
  }

  // Return the |updatecheck| element status as a parsing error.
  *error = result->status;
  return true;
}

bool ParseApp(const base::Value& node_value,
              ProtocolParser::App* result,
              std::string* error) {
  if (!node_value.is_dict()) {
    *error = "'app' is not a dictionary.";
    return false;
  }
  const base::DictValue& node = node_value.GetDict();

  result->cohort = ParseOptional(node, "cohort");
  result->cohort_name = ParseOptional(node, "cohortname");
  result->cohort_hint = ParseOptional(node, "cohorthint");
  base::expected<std::string, std::string> appid = Parse(node, "appid");
  if (!appid.has_value()) {
    *error = appid.error();
    return false;
  }
  result->app_id = appid.value();
  base::expected<std::string, std::string> status = Parse(node, "status");
  if (!status.has_value()) {
    *error = status.error();
    return false;
  }
  result->status = status.value();

  if (result->status == "ok") {
    if (const base::ListValue* data_node = node.FindList("data")) {
      std::ranges::for_each(*data_node, [&result](const base::Value& data) {
        ParseData(data, result);
      });
    }
    return ParseUpdateCheck(node.Find("updatecheck"), result, error);
  }

  return true;
}

}  // namespace

bool ProtocolParserJSON::DoParse(std::string_view response_json,
                                 Results* results) {
  CHECK(results);

  if (response_json.empty()) {
    ParseError("Empty JSON.");
    return false;
  }

  // The JSON response contains a prefix to prevent XSSI.
  static constexpr std::string_view kJSONPrefix = ")]}'";
  if (!response_json.starts_with(kJSONPrefix)) {
    ParseError("Missing secure JSON prefix.");
    return false;
  }
  const auto doc = base::JSONReader::ReadDict(
      response_json.substr(kJSONPrefix.size()), base::JSON_PARSE_RFC);
  if (!doc) {
    ParseError("JSON read error.");
    return false;
  }
  const base::DictValue* response_node = doc->FindDict("response");
  if (!response_node) {
    ParseError("Missing 'response' element or 'response' is not a dictionary.");
    return false;
  }
  const std::string* protocol = response_node->FindString("protocol");
  if (!protocol) {
    ParseError("Missing/non-string protocol.");
    return false;
  }
  if (*protocol != protocol_request::kProtocolVersion) {
    ParseError(
        absl::StrFormat("Incorrect protocol. (expected '%s', found '%s')",
                        protocol_request::kProtocolVersion, *protocol));
    return false;
  }

  const base::DictValue* daystart_node = response_node->FindDict("daystart");
  if (daystart_node) {
    const std::optional<int> elapsed_days =
        daystart_node->FindInt("elapsed_days");
    if (elapsed_days) {
      results->daystart_elapsed_days = *elapsed_days;
    }
  }

  const base::ListValue* app_node = response_node->FindList("apps");
  if (app_node) {
    for (const auto& app : *app_node) {
      App result;
      std::string error;
      if (ParseApp(app, &result, &error)) {
        results->apps.push_back(result);
      } else {
        ParseError(error);
      }
    }
  }

  return true;
}

base::expected<ProtocolParser::Results, std::string>
ProtocolParserJSON::ParseJSON(const std::string& json) {
  ProtocolParserJSON parser;
  if (parser.Parse(json)) {
    return parser.results();
  }
  return base::unexpected(parser.errors());
}

}  // namespace update_client
