#include "third_party/blink/renderer/modules/credentialmanagement/json.h"

#include "base/containers/span.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_inputs_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_client_outputs_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_cmtg_key_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_cmtg_key_outputs_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_large_blob_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_large_blob_inputs_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_large_blob_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_inputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_inputs_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_outputs.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_values.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_extensions_prf_values_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_creation_options_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_descriptor.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_descriptor_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_request_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_request_options_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_user_entity.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_user_entity_js_on.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_base.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_piece.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/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/heap_traits.h"
#include "third_party/blink/renderer/platform/wtf/text/base64.h"

namespace blink {

namespace {

std::optional<DOMArrayBuffer*> WebAuthnBase64UrlDecode(const String& in) {
  VectorOf<uint8_t> out;
  if (!Base64UnpaddedUrlDecode(in, out)) {
    return std::nullopt;
  }
  return DOMArrayBuffer::Create(base::as_byte_span(out));
}

PublicKeyCredentialUserEntity* PublicKeyCredentialUserEntityFromJSON(
    const PublicKeyCredentialUserEntityJSON& json,
    ExceptionState& exception_state) {
  auto* result = PublicKeyCredentialUserEntity::Create();
  if (auto id = WebAuthnBase64UrlDecode(json.id()); id.has_value()) {
    result->setId(
        MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(*id));
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kEncodingError,
        "'user.id' contains invalid base64url data");
    return nullptr;
  }
  result->setName(json.name());
  result->setDisplayName(json.displayName());
  return result;
}

PublicKeyCredentialDescriptor* PublicKeyCredentialDescriptorFromJSON(
    std::string_view field_name,
    const PublicKeyCredentialDescriptorJSON& json,
    ExceptionState& exception_state) {
  auto* result = PublicKeyCredentialDescriptor::Create();
  if (auto id = WebAuthnBase64UrlDecode(json.id()); id.has_value()) {
    result->setId(
        MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(*id));
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kEncodingError,
        String(base::StrCat({"'", field_name,
                             "' contains PublicKeyCredentialDescriptorJSON "
                             "with invalid base64url data in 'id'"})));
    return nullptr;
  }
  result->setType(json.type());
  if (json.hasTransports()) {
    Vector<String> transports;
    for (const String& transport : json.transports()) {
      transports.push_back(transport);
    }
    result->setTransports(std::move(transports));
  }
  return result;
}

VectorOf<PublicKeyCredentialDescriptor>
PublicKeyCredentialDescriptorVectorFromJSON(
    std::string_view field_name,
    const VectorOf<PublicKeyCredentialDescriptorJSON> json,
    ExceptionState& exception_state) {
  VectorOf<PublicKeyCredentialDescriptor> result;
  for (const PublicKeyCredentialDescriptorJSON* json_descriptor : json) {
    auto* descriptor = PublicKeyCredentialDescriptorFromJSON(
        field_name, *json_descriptor, exception_state);
    if (exception_state.HadException()) {
      return {};
    }
    result.push_back(descriptor);
  }
  return result;
}

std::optional<AuthenticationExtensionsPRFValues*>
AuthenticationExtensionsPRFValuesFromJSON(
    const AuthenticationExtensionsPRFValuesJSON& json) {
  auto* values = AuthenticationExtensionsPRFValues::Create();
  auto first = WebAuthnBase64UrlDecode(json.first());
  if (!first.has_value()) {
    return std::nullopt;
  }
  values->setFirst(
      MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(first.value()));
  if (json.hasSecond()) {
    auto second = WebAuthnBase64UrlDecode(json.second());
    if (!second.has_value()) {
      return std::nullopt;
    }
    values->setSecond(MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(
        second.value()));
  }
  return values;
}

AuthenticationExtensionsClientInputs*
AuthenticationExtensionsClientInputsFromJSON(
    const AuthenticationExtensionsClientInputsJSON& json,
    ExceptionState& exception_state) {
  auto* result = AuthenticationExtensionsClientInputs::Create();
  if (json.hasAppid()) {
    result->setAppid(json.appid());
  }
  if (json.hasAppidExclude()) {
    result->setAppidExclude(json.appidExclude());
  }
  if (json.hasHmacCreateSecret()) {
    result->setHmacCreateSecret(json.hmacCreateSecret());
  }
  if (json.hasCredentialProtectionPolicy()) {
    result->setCredentialProtectionPolicy(json.credentialProtectionPolicy());
  }
  if (json.hasEnforceCredentialProtectionPolicy()) {
    result->setEnforceCredentialProtectionPolicy(
        json.enforceCredentialProtectionPolicy());
  }
  if (json.hasMinPinLength()) {
    result->setMinPinLength(json.minPinLength());
  }
  result->setCredProps(json.credProps());
  if (json.hasLargeBlob()) {
    auto* large_blob = AuthenticationExtensionsLargeBlobInputs::Create();
    if (json.largeBlob()->hasSupport()) {
      large_blob->setSupport(json.largeBlob()->support());
    }
    if (json.largeBlob()->hasRead()) {
      large_blob->setRead(json.largeBlob()->read());
    }
    if (json.largeBlob()->hasWrite()) {
      if (auto write = WebAuthnBase64UrlDecode(json.largeBlob()->write());
          write.has_value()) {
        large_blob->setWrite(
            MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(
                write.value()));
      } else {
        exception_state.ThrowDOMException(
            DOMExceptionCode::kEncodingError,
            "'extensions.largeBlob.write' contains invalid base64url data");
        return nullptr;
      }
    }
    result->setLargeBlob(large_blob);
  }
  if (json.hasCredBlob()) {
    if (auto cred_blob = WebAuthnBase64UrlDecode(json.credBlob());
        cred_blob.has_value()) {
      result->setCredBlob(
          MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(
              cred_blob.value()));
    } else {
      exception_state.ThrowDOMException(
          DOMExceptionCode::kEncodingError,
          "'extensions.credBlob' contains invalid base64url data");
      return nullptr;
    }
  }
  if (json.hasGetCredBlob()) {
    result->setGetCredBlob(json.getCredBlob());
  }
  if (json.hasPayment()) {
    result->setPayment(json.payment());
  }
  if (json.hasRemoteDesktopClientOverride()) {
    result->setRemoteDesktopClientOverride(json.remoteDesktopClientOverride());
  }
  if (json.hasPrf()) {
    auto* prf = AuthenticationExtensionsPRFInputs::Create();
    if (json.prf()->hasEval()) {
      std::optional<AuthenticationExtensionsPRFValues*> eval =
          AuthenticationExtensionsPRFValuesFromJSON(*(json.prf()->eval()));
      if (!eval) {
        exception_state.ThrowDOMException(
            DOMExceptionCode::kEncodingError,
            "'extensions.prf.eval' contains invalid base64url data");
        return nullptr;
      }
      prf->setEval(eval.value());
    }
    if (json.prf()->hasEvalByCredential()) {
      VectorOfPairs<String, AuthenticationExtensionsPRFValues> eval;
      for (const auto& [key, json_values] : json.prf()->evalByCredential()) {
        std::optional<AuthenticationExtensionsPRFValues*> values =
            AuthenticationExtensionsPRFValuesFromJSON(*json_values);
        if (!values) {
          exception_state.ThrowDOMException(
              DOMExceptionCode::kEncodingError,
              "'extensions.prf.evalByCredential' contains invalid base64url "
              "data");
          return nullptr;
        }
        eval.emplace_back(key, *values);
      }
      prf->setEvalByCredential(std::move(eval));
    }
    result->setPrf(prf);
  }
  if (json.hasCrossDeviceFallbackUrl()) {
    result->setCrossDeviceFallbackUrl(json.crossDeviceFallbackUrl());
  }
  if (json.hasCmtgKey()) {
    result->setCmtgKey(json.cmtgKey());
  }
  return result;
}

}  // namespace

String WebAuthnBase64UrlEncode(DOMArrayPiece buffer) {
  // Tell Base64URLEncode to not include padding (trailing '=').
  return Base64UrlEncode(buffer.ByteSpan(),
                         Base64UrlEncodePolicy::kOmitPadding);
}

AuthenticationExtensionsClientOutputsJSON*
AuthenticationExtensionsClientOutputsToJSON(
    ScriptState* script_state,
    const blink::AuthenticationExtensionsClientOutputs& in) {
  auto* json = AuthenticationExtensionsClientOutputsJSON::Create();
  if (in.hasAppid()) {
    json->setAppid(in.appid());
  }
  if (in.hasHmacCreateSecret()) {
    json->setHmacCreateSecret(in.hmacCreateSecret());
  }
  if (in.hasCredProps()) {
    json->setCredProps(in.credProps());
  }
  if (in.hasLargeBlob()) {
    V8ObjectBuilder builder(script_state);
    const auto* large_blob = in.largeBlob();
    if (large_blob->hasSupported()) {
      builder.AddBoolean("supported", large_blob->supported());
    }
    if (large_blob->hasBlob()) {
      builder.AddString("blob", WebAuthnBase64UrlEncode(large_blob->blob()));
    }
    if (large_blob->hasWritten()) {
      builder.AddBoolean("written", large_blob->written());
    }
    json->setLargeBlob(builder.ToScriptObject());
  }
  if (in.hasCredBlob()) {
    json->setCredBlob(in.credBlob());
  }
  if (in.hasGetCredBlob()) {
    json->setGetCredBlob(WebAuthnBase64UrlEncode(in.getCredBlob()));
  }
  if (in.hasPrf()) {
    V8ObjectBuilder builder(script_state);
    const AuthenticationExtensionsPRFOutputs& prf = *in.prf();
    if (prf.hasEnabled()) {
      builder.AddBoolean("enabled", prf.enabled());
    }
    if (prf.hasResults()) {
      V8ObjectBuilder results_builder(script_state);
      results_builder.AddString(
          "first", WebAuthnBase64UrlEncode(prf.results()->first()));
      if (prf.results()->hasSecond()) {
        results_builder.AddString(
            "second", WebAuthnBase64UrlEncode(prf.results()->second()));
      }
      builder.Add("results", results_builder);
    }
    json->setPrf(builder.ToScriptObject());
  }
  if (in.hasCrossDeviceFallbackUrl()) {
    json->setCrossDeviceFallbackUrl(in.crossDeviceFallbackUrl());
  }
  if (in.hasCmtgKey()) {
    auto* cmtg_key_json = AuthenticationExtensionsCmtgKeyOutputsJSON::Create();
    cmtg_key_json->setCmtgKey(WebAuthnBase64UrlEncode(in.cmtgKey()->cmtgKey()));
    cmtg_key_json->setSignature(
        WebAuthnBase64UrlEncode(in.cmtgKey()->signature()));
    json->setCmtgKey(cmtg_key_json);
  }
  return json;
}

PublicKeyCredentialCreationOptions* PublicKeyCredentialCreationOptionsFromJSON(
    const PublicKeyCredentialCreationOptionsJSON* json,
    ExceptionState& exception_state) {
  auto* result = PublicKeyCredentialCreationOptions::Create();
  result->setRp(json->rp());
  auto* user =
      PublicKeyCredentialUserEntityFromJSON(*json->user(), exception_state);
  if (exception_state.HadException()) {
    return nullptr;
  }
  result->setUser(user);
  if (auto challenge = WebAuthnBase64UrlDecode(json->challenge());
      challenge.has_value()) {
    result->setChallenge(
        MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(*challenge));
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kEncodingError,
        "'challenge' contains invalid base64url data");
    return nullptr;
  }
  result->setPubKeyCredParams(json->pubKeyCredParams());
  if (json->hasTimeout()) {
    result->setTimeout(json->timeout());
  }
  if (json->hasExcludeCredentials()) {
    VectorOf<PublicKeyCredentialDescriptor> credential_descriptors =
        PublicKeyCredentialDescriptorVectorFromJSON(
            "excludeCredentials", json->excludeCredentials(), exception_state);
    if (exception_state.HadException()) {
      return nullptr;
    }
    result->setExcludeCredentials(std::move(credential_descriptors));
  }
  if (json->hasAuthenticatorSelection()) {
    result->setAuthenticatorSelection(json->authenticatorSelection());
  }
  if (json->hasHints()) {
    result->setHints(json->hints());
  }
  if (json->hasAttestation()) {
    result->setAttestation(json->attestation());
  }
  if (json->hasExtensions()) {
    auto* extensions = AuthenticationExtensionsClientInputsFromJSON(
        *json->extensions(), exception_state);
    if (exception_state.HadException()) {
      return nullptr;
    }
    result->setExtensions(extensions);
  }
  return result;
}

PublicKeyCredentialRequestOptions* PublicKeyCredentialRequestOptionsFromJSON(
    const PublicKeyCredentialRequestOptionsJSON* json,
    ExceptionState& exception_state) {
  auto* result = PublicKeyCredentialRequestOptions::Create();
  if (auto challenge = WebAuthnBase64UrlDecode(json->challenge());
      challenge.has_value()) {
    result->setChallenge(
        MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferView>(*challenge));
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kEncodingError,
        "'challenge' contains invalid base64url data");
    return nullptr;
  }
  if (json->hasTimeout()) {
    result->setTimeout(json->timeout());
  }
  if (json->hasRpId()) {
    result->setRpId(json->rpId());
  }
  if (json->hasAllowCredentials()) {
    VectorOf<PublicKeyCredentialDescriptor> credential_descriptors =
        PublicKeyCredentialDescriptorVectorFromJSON(
            "allowCredentials", json->allowCredentials(), exception_state);
    if (exception_state.HadException()) {
      return nullptr;
    }
    result->setAllowCredentials(std::move(credential_descriptors));
  }
  if (json->hasUserVerification()) {
    result->setUserVerification(json->userVerification());
  }
  if (json->hasHints()) {
    result->setHints(json->hints());
  }
  if (json->hasExtensions()) {
    auto* extensions = AuthenticationExtensionsClientInputsFromJSON(
        *json->extensions(), exception_state);
    if (exception_state.HadException()) {
      return nullptr;
    }
    result->setExtensions(extensions);
  }
  return result;
}

}  // namespace blink
