// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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 org.openqa.selenium.environment.webserver;

import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static java.util.Collections.singletonMap;
import static org.openqa.selenium.json.Json.JSON_UTF_8;
import static org.openqa.selenium.remote.http.Contents.string;

import com.google.common.collect.ImmutableMap;
import dev.failsafe.Failsafe;
import dev.failsafe.RetryPolicy;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.temporal.ChronoUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openqa.selenium.grid.config.CompoundConfig;
import org.openqa.selenium.grid.config.Config;
import org.openqa.selenium.grid.config.MapConfig;
import org.openqa.selenium.grid.config.MemoizedConfig;
import org.openqa.selenium.grid.server.BaseServerOptions;
import org.openqa.selenium.grid.server.Server;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.io.TemporaryFilesystem;
import org.openqa.selenium.net.PortProber;
import org.openqa.selenium.netty.server.NettyServer;
import org.openqa.selenium.netty.server.ServerBindException;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpMethod;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

public class NettyAppServer implements AppServer {

  private static final Config sslConfig =
      new MapConfig(singletonMap("server", singletonMap("https-self-signed", true)));
  private static final Logger LOG = Logger.getLogger(NettyAppServer.class.getName());
  private Server<?> server;
  private Server<?> secure;
  private final RetryPolicy<Object> retryPolicy =
      RetryPolicy.builder()
          .withMaxAttempts(5)
          .withDelay(100, 1000, ChronoUnit.MILLIS)
          .handle(ServerBindException.class)
          .onRetry(
              e -> {
                LOG.log(
                    Level.WARNING,
                    String.format("NettyAppServer retry #%s. ", e.getAttemptCount()));
                initValues(secure != null);
              })
          .onRetriesExceeded(e -> LOG.log(Level.WARNING, "NettyAppServer start aborted."))
          .build();

  public NettyAppServer(boolean secureServer) {
    initValues(secureServer);
  }

  public NettyAppServer(HttpHandler handler) {
    this(createDefaultConfig(), Require.nonNull("Handler", handler));
  }

  private NettyAppServer(Config config, HttpHandler handler) {
    Require.nonNull("Config", config);
    Require.nonNull("Handler", handler);

    server = new NettyServer(new BaseServerOptions(new MemoizedConfig(config)), handler);
    secure = null;
  }

  private static Config createDefaultConfig() {
    return new MemoizedConfig(
        new MapConfig(singletonMap("server", singletonMap("port", PortProber.findFreePort()))));
  }

  public static void main(String[] args) {
    int port = 2310;
    if (args.length > 0) {
      port = Integer.parseInt(args[0]);
    }

    MemoizedConfig config =
        new MemoizedConfig(new MapConfig(singletonMap("server", singletonMap("port", port))));
    BaseServerOptions options = new BaseServerOptions(config);

    HttpHandler handler =
        new HandlersForTests(
            options.getHostname().orElse("localhost"),
            options.getPort(),
            TemporaryFilesystem.getDefaultTmpFS().createTempDir("netty", "server").toPath());

    NettyAppServer server = new NettyAppServer(config, handler);
    server.start();

    System.out.printf("Server started. Root URL: %s%n", server.whereIs("/"));
  }

  private void initValues(boolean secureServer) {
    Config config = createDefaultConfig();
    BaseServerOptions options = new BaseServerOptions(config);

    File tempDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("generated", "pages");

    HttpHandler handler =
        new HandlersForTests(
            options.getHostname().orElse("localhost"), options.getPort(), tempDir.toPath());

    server = new NettyServer(options, handler);

    if (secureServer) {
      Config secureConfig = new CompoundConfig(sslConfig, createDefaultConfig());
      BaseServerOptions secureOptions = new BaseServerOptions(secureConfig);

      HttpHandler secureHandler =
          new HandlersForTests(
              secureOptions.getHostname().orElse("localhost"),
              secureOptions.getPort(),
              tempDir.toPath());

      secure = new NettyServer(secureOptions, secureHandler);
    }
  }

  @Override
  public void start() {
    Failsafe.with(retryPolicy)
        .run(
            () -> {
              server.start();
              if (secure != null) {
                try {
                  secure.start();
                } catch (Exception e) {
                  try {
                    server.stop();
                  } catch (Exception ex) {
                    e.addSuppressed(ex);
                    throw e;
                  }
                }
              }
            });
  }

  @Override
  public void stop() {
    server.stop();
    if (secure != null) {
      secure.stop();
    }
  }

  @Override
  public String whereIs(String relativeUrl) {
    return createUrl(server, "http", getHostName(), relativeUrl);
  }

  @Override
  public String whereElseIs(String relativeUrl) {
    return createUrl(server, "http", getAlternateHostName(), relativeUrl);
  }

  @Override
  public String whereIsSecure(String relativeUrl) {
    if (secure == null) {
      throw new IllegalStateException("Server not configured to return HTTPS url");
    }
    return createUrl(secure, "https", getHostName(), relativeUrl);
  }

  @Override
  public String whereIsWithCredentials(String relativeUrl, String user, String password) {
    return String.format(
        "http://%s:%s@%s:%d/%s",
        user, password, getHostName(), server.getUrl().getPort(), relativeUrl);
  }

  private String createUrl(Server<?> server, String protocol, String hostName, String relativeUrl) {
    if (!relativeUrl.startsWith("/")) {
      relativeUrl = "/" + relativeUrl;
    }

    try {
      return new URL(protocol, hostName, server.getUrl().getPort(), relativeUrl).toString();
    } catch (MalformedURLException e) {
      throw new UncheckedIOException(e);
    }
  }

  @Override
  public String create(Page page) {
    try (HttpClient client =
        HttpClient.Factory.createDefault().createClient(new URL(whereIs("/")))) {
      HttpRequest request = new HttpRequest(HttpMethod.POST, "/common/createPage");
      request.setHeader(CONTENT_TYPE, JSON_UTF_8);
      request.setContent(Contents.asJson(ImmutableMap.of("content", page.toString())));
      HttpResponse response = client.execute(request);
      return string(response);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  @Override
  public String getHostName() {
    return AppServer.detectHostname();
  }

  @Override
  public String getAlternateHostName() {
    return AppServer.detectAlternateHostname();
  }
}
