/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.http.javadsl.server;

import org.apache.pekko.NotUsed;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.http.javadsl.Http;
import org.apache.pekko.http.javadsl.ServerBinding;
import org.apache.pekko.http.javadsl.common.EntityStreamingSupport;
import org.apache.pekko.http.javadsl.marshallers.jackson.Jackson;
import org.apache.pekko.http.javadsl.model.HttpRequest;
import org.apache.pekko.http.javadsl.model.HttpResponse;
import org.apache.pekko.http.javadsl.model.StatusCodes;
import org.apache.pekko.http.javadsl.server.directives.SecurityDirectives.ProvidedCredentials;
import org.apache.pekko.http.javadsl.unmarshalling.StringUnmarshallers;
import org.apache.pekko.http.javadsl.unmarshalling.Unmarshaller;
import org.apache.pekko.stream.javadsl.Source;
import org.apache.pekko.util.ByteString;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.Function;

import static org.apache.pekko.http.javadsl.server.Directives.*;

public class JavaTestServer {

  public Route createRoute() {
    final Duration timeout = Duration.ofSeconds(1);

    final Route index =
        path(
            "",
            () ->
                withRequestTimeout(
                    timeout,
                    this::mkTimeoutResponse,
                    () -> {
                      silentSleep(5000); // too long, but note that this will NOT activate
                      // withRequestTimeout, see below
                      return complete(index());
                    }));

    final Route requestTimeout =
        path(
            "timeout",
            () ->
                withRequestTimeout(
                    timeout,
                    this::mkTimeoutResponse,
                    () -> {
                      // here timeout will work
                      return completeOKWithFutureString(neverEndingFuture(index()));
                    }));

    final Function<Optional<ProvidedCredentials>, Optional<String>> handleAuth =
        (maybeCreds) -> {
          if (maybeCreds.isPresent()
              && maybeCreds.get().verify("pa$$word")) // some secure hash + check
          return Optional.of(maybeCreds.get().identifier());
          else return Optional.empty();
        };

    final Route secure =
        path(
            "secure",
            () ->
                authenticateBasic(
                    "My basic secure site",
                    handleAuth,
                    (login) -> complete(String.format("Hello, %s!", login))));

    final Route ping = path("ping", () -> complete("PONG!"));

    final Route crash =
        path(
            "crash",
            () ->
                path(
                        "scala",
                        () ->
                            completeOKWithFutureString(
                                org.apache.pekko.dispatch.Futures.failed(new Exception("Boom!"))))
                    .orElse(
                        path(
                            "java",
                            () ->
                                completeOKWithFutureString(
                                    CompletableFuture.supplyAsync(
                                        () -> {
                                          throw new RuntimeException("Boom!");
                                        })))));

    final Unmarshaller<ByteString, JavaTweet> JavaTweets =
        Jackson.byteStringUnmarshaller(JavaTweet.class);
    final Route tweets =
        path(
            "tweets",
            () ->
                get(() ->
                        parameter(
                            StringUnmarshallers.INTEGER,
                            "n",
                            n -> {
                              final Source<JavaTweet, NotUsed> tws =
                                  Source.repeat(new JavaTweet("Hello World!")).take(n);
                              return completeOKWithSource(
                                  tws, Jackson.marshaller(), EntityStreamingSupport.json());
                            }))
                    .orElse(
                        post(
                            () ->
                                extractMaterializer(
                                    mat ->
                                        entityAsSourceOf(
                                            JavaTweets,
                                            null,
                                            sourceOfTweets -> {
                                              final CompletionStage<Integer> tweetsCount =
                                                  sourceOfTweets.runFold(
                                                      0, (acc, tweet) -> acc + 1, mat);
                                              return onComplete(
                                                  tweetsCount,
                                                  c -> complete("Total number of tweets: " + c));
                                            })))));

    final Route inner = path("inner", () -> getFromResourceDirectory("someDir"));

    return index
        .orElse(secure)
        .orElse(ping)
        .orElse(crash)
        .orElse(inner)
        .orElse(requestTimeout)
        .orElse(tweets);
  }

  private void silentSleep(int millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  private CompletableFuture<String> neverEndingFuture(String futureContent) {
    return new CompletableFuture<>().thenApply((string) -> futureContent);
  }

  private HttpResponse mkTimeoutResponse(HttpRequest request) {
    return HttpResponse.create()
        .withStatus(StatusCodes.ENHANCE_YOUR_CALM)
        .withEntity("Unable to serve response within time limit, please enhance your calm.");
  }

  private String index() {
    return "    <html>\n"
        + "      <body>\n"
        + "        <h1>Say hello to <i>pekko-http-core</i>!</h1>\n"
        + "        <p>Defined resources:</p>\n"
        + "        <ul>\n"
        + "          <li><a href=\"/ping\">/ping</a></li>\n"
        + "          <li><a href=\"/secure\">/secure</a> Use any username and '&lt;username&gt;-password' as credentials</li>\n"
        + "          <li><a href=\"/crash\">/crash</a></li>\n"
        + "          <li><a href=\"/timeout\">/timeout</a> Demonstrates timeout </li>\n"
        + "        </ul>\n"
        + "      </body>\n"
        + "    </html>\n";
  }

  public static void main(String[] args) throws InterruptedException {
    final JavaTestServer server = new JavaTestServer();
    server.run();
  }

  private void run() throws InterruptedException {
    final ActorSystem system = ActorSystem.create();

    final CompletionStage<ServerBinding> binding =
        Http.get(system).newServerAt("127.0.0.1", 8080).bind(createRoute());

    System.console().readLine("Press [ENTER] to quit...");
    shutdown(binding);
  }

  private CompletionStage<Void> shutdown(CompletionStage<ServerBinding> binding) {
    return binding.thenAccept(
        b -> {
          System.out.println(String.format("Unbinding from %s", b.localAddress()));

          final CompletionStage<?> unbound = b.unbind();
          try {
            unbound.toCompletableFuture().get(3, TimeUnit.SECONDS); // block...
          } catch (TimeoutException | InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
          }
        });
  }

  private static final class JavaTweet {
    private String message;

    public JavaTweet(String message) {
      this.message = message;
    }

    public void setMessage(String message) {
      this.message = message;
    }

    public String getMessage() {
      return message;
    }
  }
}
