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

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.net.PercentEscaper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nonnull;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.Validate;

public final class UrlUtils {
   private static final String URL_PATH_SAFE_CHARS = "-._~!$'()*,;&=+";

   private UrlUtils() {
   }

   public static String addParameterToUrl(String url, String paramName, String paramValue) {
      if (StringUtils.isBlank(paramName)) {
         return url;
      }

      UrlUtils.UrlParts urlParts = UrlUtils.UrlParts.split(url);
      String paramPart = createParameterPart(paramName, paramValue);
      Optional<String> newQuery = Optional.of(urlParts.getQuery().map(query -> query + "&" + paramPart).orElse(paramPart));
      return UrlUtils.UrlParts.assemble(urlParts.getUrl(), newQuery, urlParts.getFragment());
   }

   private static String createParameterPart(String paramName, String paramValue) {
      return StringUtils.isBlank(paramValue) ? urlEncodeUrlParameter(paramName) : urlEncodeUrlParameter(paramName) + "=" + urlEncodeUrlParameter(paramValue);
   }

   public static String omitQuery(String url) {
      return UrlUtils.UrlParts.split(url).getUrl();
   }

   public static String appendPathElementToUrl(String url, String pathElement) {
      UrlUtils.UrlParts urlParts = UrlUtils.UrlParts.split(url);
      if (StringUtils.isEmpty(pathElement)) {
         return url;
      }

      String baseUrl = Strings.CS.removeEnd(urlParts.getUrl(), "/");
      String newPathElement = Strings.CS.removeStart(pathElement, "/");
      String newUrl = baseUrl + "/" + newPathElement;
      return UrlUtils.UrlParts.assemble(newUrl, urlParts.getQuery(), urlParts.getFragment());
   }

   public static String replaceOrAddParameterInUrl(String url, String paramName, String newValue) {
      Objects.requireNonNull(url, "url is null.");
      Objects.requireNonNull(paramName, "paramName is null.");
      String urlWithRemovedParametersOfNameParamName = removeParametersWithName(url, paramName);
      return addParameterToUrl(urlWithRemovedParametersOfNameParamName, paramName, newValue);
   }

   public static String removeParametersWithName(String url, String paramName) {
      Objects.requireNonNull(paramName, "paramName is null.");
      UrlUtils.UrlParts urlParts = UrlUtils.UrlParts.split(url);
      Optional<String> newQuery = urlParts.getQuery()
         .map(q -> StringUtils.trimToNull(removeAllParametersWithNameFromQueryString(q, urlEncodeUrlParameter(paramName))));
      return UrlUtils.UrlParts.assemble(urlParts.getUrl(), newQuery, urlParts.getFragment());
   }

   private static String removeAllParametersWithNameFromQueryString(String queryString, String urlEncodedparamName) {
      Matcher matcher = Pattern.compile("((^|&)" + Pattern.quote(urlEncodedparamName) + "(=[^&]*|$|(?=&)))").matcher(queryString);
      if (matcher.find()) {
         String queryStringWithRemovedParams = matcher.replaceAll("");
         return queryStringWithRemovedParams.startsWith("&") ? StringUtils.substring(queryStringWithRemovedParams, 1) : queryStringWithRemovedParams;
      } else {
         return queryString;
      }
   }

   public static Optional<String> getUrlParameterValue(String param, String url) {
      List<String> values = extractUrlParameters(url).get(param);
      return CollectionUtils.isEmpty(values) ? Optional.empty() : Optional.of(values.getFirst());
   }

   public static Map<String, List<String>> extractUrlParameters(String url) {
      String query = UrlUtils.UrlParts.split(url).getQuery().orElse("");
      Iterable<String> pairs = Splitter.on('&').trimResults().split(query);
      Map<String, List<String>> params = Maps.newLinkedHashMap();

      for (String pair : pairs) {
         List<String> keyValuePair = Splitter.on('=').trimResults().splitToList(pair);
         if (keyValuePair.size() == 2) {
            String key = urlDecode(keyValuePair.getFirst());
            if (!params.containsKey(key)) {
               List<String> values = Lists.newLinkedList();
               params.put(key, values);
            }

            List<String> values = params.get(key);
            String value = urlDecode(keyValuePair.get(1));
            values.add(value);
         }
      }

      return params;
   }

   public static boolean hasParameter(String url, String param) {
      return extractUrlParameters(url).containsKey(param);
   }

   public static String urlEncodeFormPost(@Nonnull String string) {
      return URLEncoder.encode(string, StandardCharsets.UTF_8);
   }

   public static String urlEncodeFormPost(@Nonnull String string, @Nonnull Charset charset) {
      return URLEncoder.encode(string, charset);
   }

   public static String urlEncodeUrlParameter(@Nonnull String string) {
      return urlEncodeUrlParameter(string, StandardCharsets.UTF_8);
   }

   public static String urlEncodeUrlParameter(@Nonnull String string, @Nonnull Charset charset) {
      return URLEncoder.encode(string, charset).replace("+", "%20");
   }

   public static String urlEncodeCookie(@Nonnull String string) {
      return urlEncodeCookie(string, StandardCharsets.UTF_8);
   }

   public static String urlEncodeCookie(@Nonnull String string, @Nonnull Charset charset) {
      return urlEncodeUrlParameter(string, charset);
   }

   public static String urlEncodePathSegment(@Nonnull String string) {
      return new PercentEscaper("-._~!$'()*,;&=+", false).escape(string);
   }

   public static String urlDecode(@Nonnull String string) {
      return urlDecode(string, StandardCharsets.UTF_8);
   }

   public static String urlDecode(@Nonnull String string, @Nonnull Charset charset) {
      return URLDecoder.decode(string, charset);
   }

   public static String urlDecodePathSegment(@Nonnull String string) {
      String fixedString = string.replace("+", "%2B");
      return urlDecode(fixedString, StandardCharsets.UTF_8);
   }

   static class UrlParts {
      private final String url;
      private final Optional<String> query;
      private final Optional<String> fragment;

      static UrlUtils.UrlParts split(String url) {
         Validate.notEmpty(url, "url must not be null or empty", new Object[0]);
         return builder()
            .url(UrlUtils.UrlParts.UrlSplitter.getUrl(url))
            .query(UrlUtils.UrlParts.UrlSplitter.getQuery(url))
            .fragment(UrlUtils.UrlParts.UrlSplitter.getFragment(url))
            .build();
      }

      static String assemble(String url, Optional<String> query, Optional<String> fragment) {
         return url + query.<String>map(q -> "?" + q).orElse("") + fragment.<String>map(f -> "#" + f).orElse("");
      }

      @SuppressFBWarnings(justification = "generated code")
      UrlParts(String url, Optional<String> query, Optional<String> fragment) {
         this.url = url;
         this.query = query;
         this.fragment = fragment;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static UrlUtils.UrlParts.UrlPartsBuilder builder() {
         return new UrlUtils.UrlParts.UrlPartsBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getUrl() {
         return this.url;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Optional<String> getQuery() {
         return this.query;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Optional<String> getFragment() {
         return this.fragment;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class UrlPartsBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private String url;
         @SuppressFBWarnings(justification = "generated code")
         private Optional<String> query;
         @SuppressFBWarnings(justification = "generated code")
         private Optional<String> fragment;

         @SuppressFBWarnings(justification = "generated code")
         UrlPartsBuilder() {
         }

         @SuppressFBWarnings(justification = "generated code")
         public UrlUtils.UrlParts.UrlPartsBuilder url(String url) {
            this.url = url;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UrlUtils.UrlParts.UrlPartsBuilder query(Optional<String> query) {
            this.query = query;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UrlUtils.UrlParts.UrlPartsBuilder fragment(Optional<String> fragment) {
            this.fragment = fragment;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UrlUtils.UrlParts build() {
            return new UrlUtils.UrlParts(this.url, this.query, this.fragment);
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "UrlUtils.UrlParts.UrlPartsBuilder(url=" + this.url + ", query=" + this.query + ", fragment=" + this.fragment + ")";
         }
      }

      private static class UrlSplitter {
         private static String getUrl(String fullUrl) {
            String url = fullUrl;
            if (Strings.CS.contains(url, "#")) {
               url = StringUtils.substringBefore(url, "#");
            }

            if (Strings.CS.contains(url, "?")) {
               url = StringUtils.substringBefore(url, "?");
            }

            return url;
         }

         private static Optional<String> getQuery(String url) {
            String urlAndQuery = StringUtils.substringBefore(url, "#");
            return Optional.ofNullable(StringUtils.trimToNull(StringUtils.substringAfter(urlAndQuery, "?")));
         }

         private static Optional<String> getFragment(String url) {
            return Optional.ofNullable(StringUtils.trimToNull(StringUtils.substringAfter(url, "#")));
         }
      }
   }
}
