package com.airlock.iam.base.domain.service.uri;

import com.airlock.iam.base.misc.collection.Pair;
import com.google.common.net.InetAddresses;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class IamUriBuilder {
   private static final String SLASH = "/";
   private Optional<String> path = Optional.empty();
   private Optional<String> scheme = Optional.empty();
   private Optional<String> schemaSpecificPartForOpaque = Optional.empty();
   private Optional<String> userInfo = Optional.empty();
   private Optional<String> host = Optional.empty();
   private Optional<String> port = Optional.empty();
   private List<Pair<String, String>> queryParameters = new ArrayList<>();
   private Optional<String> fragment = Optional.empty();

   private IamUriBuilder() {
   }

   public static IamUriBuilder uriBuilder() {
      return new IamUriBuilder();
   }

   public static IamUriBuilder fromUri(URI uri) {
      IamUriBuilder iamUriBuilder = new IamUriBuilder();
      iamUriBuilder.fragment = Optional.ofNullable(uri.getRawFragment());
      if (uri.isOpaque()) {
         iamUriBuilder.scheme = Optional.ofNullable(uri.getScheme());
         iamUriBuilder.schemaSpecificPartForOpaque = Optional.of(uri.getRawSchemeSpecificPart());
         return iamUriBuilder;
      }

      iamUriBuilder.scheme = Optional.ofNullable(uri.getScheme());
      if (uri.getRawAuthority() != null) {
         iamUriBuilder.userInfo = Optional.ofNullable(uri.getRawUserInfo());
         iamUriBuilder.host = Optional.ofNullable(uri.getHost());
         if (uri.getPort() != -1) {
            iamUriBuilder.port = Optional.of(String.valueOf(uri.getPort()));
         }
      }

      if (uri.getRawPath() != null && !uri.getRawPath().isEmpty()) {
         iamUriBuilder.path = Optional.of(uri.getRawPath());
      }

      if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) {
         iamUriBuilder.queryParameters = splitQueryParameters(uri.getRawQuery());
      }

      return iamUriBuilder;
   }

   public static IamUriBuilder fromUri(String uriString) {
      URI uri = URI.create(uriString);
      return fromUri(uri);
   }

   public static IamUriBuilder fromPath(String path) {
      return new IamUriBuilder().path(path);
   }

   public URI build() {
      String uri = this.schemeToString();
      if (this.schemaSpecificPartForOpaque.isPresent()) {
         uri = uri + this.schemaSpecificPartForOpaque.get();
      } else {
         uri = uri + this.authorityToString() + this.pathToString() + this.queryParametersToString();
      }

      uri = uri + this.fragmentToString();
      return URI.create(uri);
   }

   public IamUriBuilder scheme(String scheme) {
      this.scheme = Optional.ofNullable(scheme);
      return this;
   }

   public IamUriBuilder userInfo(String user) {
      this.verifyUriIsNotOpaque();
      if (user == null) {
         this.userInfo = Optional.empty();
         return this;
      } else {
         this.userInfo = Optional.of(this.encode(user, UriComponent.Type.USER_INFO));
         return this;
      }
   }

   public IamUriBuilder host(String host) {
      this.verifyUriIsNotOpaque();
      if (host == null) {
         this.host = Optional.empty();
         return this;
      }

      if (!InetAddresses.isMappedIPv4Address(host) && !InetAddresses.isUriInetAddress(host)) {
         this.host = Optional.of(this.encode(host, UriComponent.Type.HOST));
      } else {
         this.host = Optional.of(host);
      }

      return this;
   }

   public IamUriBuilder port(int port) {
      this.verifyUriIsNotOpaque();
      if (port != -1) {
         this.port = Optional.of(String.valueOf(port));
      } else {
         this.port = Optional.empty();
      }

      return this;
   }

   public IamUriBuilder replacePath(String pathToReplaceWith) {
      this.verifyUriIsNotOpaque();
      String encodedPath = this.encode(pathToReplaceWith, UriComponent.Type.PATH);
      this.path = Optional.of(encodedPath);
      return this;
   }

   public IamUriBuilder path(String pathToAppend) {
      this.verifyUriIsNotOpaque();
      String encodedPathToAppend = this.encode(pathToAppend, UriComponent.Type.PATH);
      boolean pathEndsInSlash = this.path.<Boolean>map(s -> s.endsWith("/")).orElse(true);
      boolean pathToAppendStartsInSlash = encodedPathToAppend.startsWith("/");
      if (pathEndsInSlash && pathToAppendStartsInSlash) {
         this.path = Optional.of(this.path.<String>map(p -> p + encodedPathToAppend.substring(1)).orElse(encodedPathToAppend));
         return this;
      } else if (!pathEndsInSlash && !pathToAppendStartsInSlash) {
         this.path = Optional.of(this.path.<String>map(p -> p + "/" + encodedPathToAppend).orElse(encodedPathToAppend));
         return this;
      } else {
         this.path = Optional.of(this.path.<String>map(p -> p + encodedPathToAppend).orElse(encodedPathToAppend));
         return this;
      }
   }

   public IamUriBuilder replaceQuery(String query) {
      this.verifyUriIsNotOpaque();
      this.queryParameters = splitQueryParameters(query);
      return this;
   }

   public IamUriBuilder queryParam(String name, Object... values) {
      this.verifyUriIsNotOpaque();
      if (values != null && values.length != 0) {
         String encodedName = this.encode(name, UriComponent.Type.QUERY_PARAM);
         Stream.of(values)
            .map(Object::toString)
            .map(value -> this.encode(value, UriComponent.Type.QUERY_PARAM))
            .forEach(v -> this.queryParameters.add(Pair.pair(encodedName, v)));
         return this;
      } else {
         return this;
      }
   }

   public IamUriBuilder replaceQueryParam(String name, Object... values) {
      this.verifyUriIsNotOpaque();
      String encodedName = this.encode(name, UriComponent.Type.QUERY_PARAM);
      this.queryParameters = Stream.concat(
            this.queryParameters.stream().filter(pair -> !pair.getFirst().equals(encodedName)),
            Arrays.stream(values).map(v -> Pair.pair(encodedName, this.encode(v.toString(), UriComponent.Type.QUERY_PARAM)))
         )
         .collect(Collectors.toCollection(ArrayList::new));
      return this;
   }

   public IamUriBuilder fragment(String fragment) {
      if (fragment == null) {
         this.fragment = Optional.empty();
         return this;
      } else {
         this.fragment = Optional.of(this.encode(fragment, UriComponent.Type.FRAGMENT));
         return this;
      }
   }

   private void verifyUriIsNotOpaque() {
      if (this.schemaSpecificPartForOpaque.isPresent()) {
         throw new IllegalArgumentException("Schema specific part is opaque");
      }
   }

   private String encodeQuery() {
      return this.queryParameters.stream().map(param -> param.getFirst() + "=" + param.getSecond()).collect(Collectors.joining("&"));
   }

   private String encode(String s, UriComponent.Type type) {
      return UriComponent.contextualEncode(s, type);
   }

   private boolean hasAuthority() {
      return this.userInfo.isPresent() || this.host.isPresent() || this.port.isPresent();
   }

   private static List<Pair<String, String>> splitQueryParameters(String query) {
      return query != null && !query.isEmpty()
         ? Arrays.stream(query.split("&")).map(IamUriBuilder::splitQueryParameter).collect(Collectors.toCollection(ArrayList::new))
         : new ArrayList<>();
   }

   private static Pair<String, String> splitQueryParameter(String parameter) {
      String[] parts = parameter.split("=", 2);
      if (parts.length == 2) {
         return Pair.pair(parts[0], parts[1]);
      } else {
         return parts.length == 1 && !parts[0].startsWith("=") ? Pair.pair(parts[0], "") : null;
      }
   }

   private String fragmentToString() {
      return this.fragment.<String>map(f -> "#" + f).orElse("");
   }

   private String queryParametersToString() {
      return !this.queryParameters.isEmpty() ? "?" + this.encodeQuery() : "";
   }

   private String pathToString() {
      String result = "";
      if (this.path.isPresent()) {
         if (this.hasAuthority() && this.path.get().charAt(0) != '/') {
            result = result + "/";
         }

         result = result + this.path.get();
      }

      return result;
   }

   private String authorityToString() {
      String result = "";
      if (this.hasAuthority()) {
         result = result + "//";
         result = result + this.userInfo.<String>map(user -> user + "@").orElse("");
         result = result + this.host.orElse("");
         result = result + this.port.<String>map(p -> ":" + p).orElse("");
      }

      return result;
   }

   private String schemeToString() {
      return this.scheme.<String>map(s -> s + ":").orElse("");
   }
}
