// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google.registry.tools;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static google.registry.config.RegistryConfig.CANARY_HEADER;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.Service;
import jakarta.inject.Inject;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * An HTTP connection to a service.
 *
 * <p>By default - connects the BACKEND service. To create a connection to another service, call the
 * {@link #withService} function.
 */
public class ServiceConnection {

  private final Service service;
  private final boolean useCanary;
  private final HttpRequestFactory requestFactory;
  private final Gson gson;

  @Inject
  ServiceConnection(
      @Config("useCanary") boolean useCanary, HttpRequestFactory requestFactory, Gson gson) {
    this(Service.BACKEND, requestFactory, useCanary, gson);
  }

  private ServiceConnection(
      Service service, HttpRequestFactory requestFactory, boolean useCanary, Gson gson) {
    this.service = service;
    this.requestFactory = requestFactory;
    this.useCanary = useCanary;
    this.gson = gson;
  }

  /** Returns a copy of this connection that talks to a different service endpoint. */
  public ServiceConnection withService(Service service, boolean useCanary) {
    return new ServiceConnection(service, requestFactory, useCanary, gson);
  }

  /** Returns the HTML from the connection error stream, if any, otherwise the empty string. */
  private static String getErrorHtmlAsString(HttpResponse response) throws IOException {
    return CharStreams.toString(new InputStreamReader(response.getContent(), UTF_8));
  }

  private String internalSend(
      String endpoint, Map<String, ?> params, MediaType contentType, @Nullable byte[] payload)
      throws IOException {
    GenericUrl url = new GenericUrl(String.format("%s%s", getServer(), endpoint));
    url.putAll(params);
    HttpRequest request =
        (payload != null)
            ? requestFactory.buildPostRequest(
                url, new ByteArrayContent(contentType.toString(), payload))
            : requestFactory.buildGetRequest(url);
    HttpHeaders headers = request.getHeaders();
    headers.setCacheControl("no-cache");
    headers.put(X_REQUESTED_WITH, ImmutableList.of("RegistryTool"));
    if (useCanary) {
      headers.set(CANARY_HEADER, "true");
    }
    request.setHeaders(headers);
    request.setFollowRedirects(false);
    request.setThrowExceptionOnExecuteError(false);
    request.setUnsuccessfulResponseHandler(
        (request1, response, _) -> {
          String error = getErrorHtmlAsString(response);
          throw new IOException(
              String.format(
                  "Error from %s: %d %s%s",
                  request1.getUrl().toString(),
                  response.getStatusCode(),
                  response.getStatusMessage(),
                  error));
        });
    HttpResponse response = null;
    try {
      response = request.execute();
      return CharStreams.toString(new InputStreamReader(response.getContent(), UTF_8));
    } finally {
      if (response != null) {
        response.disconnect();
      }
    }
  }

  @VisibleForTesting
  URL getServer() {
    URL url = service.getServiceUrl();
    verify(!isNullOrEmpty(url.getHost()), "Null host in url");
    return url;
  }

  public String sendPostRequest(
      String endpoint, Map<String, ?> params, MediaType contentType, byte[] payload)
      throws IOException {
    return internalSend(endpoint, params, contentType, checkNotNull(payload, "payload"));
  }

  public String sendGetRequest(String endpoint, Map<String, ?> params) throws IOException {
    return internalSend(endpoint, params, MediaType.PLAIN_TEXT_UTF_8, null);
  }

  public Map<String, Object> sendJson(String endpoint, Map<String, ?> object) throws IOException {
    String response =
        sendPostRequest(
            endpoint, ImmutableMap.of(), JSON_UTF_8, gson.toJson(object).getBytes(UTF_8));
    return gson.fromJson(response.substring(JSON_SAFETY_PREFIX.length()), new TypeToken<>() {});
  }
}
