package io.delphi.qa.auto.awsmfrw.models.api;

import com.google.common.base.CaseFormat;
import io.delphi.qa.auto.awsmfrw.common_baby.IsaLogging;
import io.delphi.qa.auto.awsmfrw.common_baby.exceptions.FeatureNotImplementedException;
import io.delphi.qa.auto.awsmfrw.managers.helpers.api.rest.AbstractRestAssuredClient;
import io.delphi.qa.auto.awsmfrw.models.api.rest.v3.EnDelphiApiV3_EndPointItems;
import io.delphi.qa.auto.awsmfrw.models.api.rest.v3.methods.params.ICanFetchHeaders;
import lombok.SneakyThrows;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

public interface ICanBuildApiTestByString extends IsaLogging, ICanFetchHeaders {

  String DEFAULT_FIELD = "fill me";
  String STR_STATUS_CODE_MARKER = "Sc"; // status_code 1XX, 2XX, 3XX, 4XX, 5XX
  String STR_EMPTY_TOKEN = null;
  String STR_EQUAL_MARKER = "Eq"; // equals =
  String STR_DATAPROVIDER_MARKER = "Arg"; // data_provider = Arg
  String STR_PATH_PARAM_MARKER = "As"; // id in curly brackets {id}
  String STR_PATH_SLASH_MARKER = "Of"; // backslash /
  String STR_DASH_PARAM_MARKER = "Dh"; // dash -
  String STR_QUERY_MARKER = "_queries";
  String STR_PATH_MARKER = "_path";
  String STR_METHOD_MARKER = "_method";
  String STR_BODY_MARKER = "_body";
  String STR_ASSERT_MARKER = "assert";
  String STR_HEADER_MARKER = "_header";
  String STR_SEPARATOR_MARKER = "Dt"; // dot sign .
  String STR_UNDERSCORE_MARKER = "_"; //

  default Map<String, String> chooseHeader(String tokenItem) {
    switch (tokenItem) {
      case "AuthBearerEmpty":
        return fetchHeadersAuthInvalidBearerEmpty();
      case "AuthBasicEmpty":
        return fetchHeadersAuthInvalidBasicEmpty();
      case "AuthEmpty":
        return fetchHeadersAuthInvalidEmpty();
      default:
        return fetchHeaderAuth0authToken();
    }
  }

  /* If header not specified then by default taken:
  'Authorization: Bearer <0authToken>'
  * */
  default Map<String, ?> deserializeHeaders(String token) {
    Map<String, String> hashMap = new HashMap<>();
    if (token != null) {
      String[] items = token.split(STR_UNDERSCORE_MARKER);
      Map<String, String> finalHashMap = hashMap;
      Arrays.stream(items)
          .forEach(
              o -> {
                Map.Entry<String, String> entry = chooseHeader(o).entrySet().iterator().next();
                finalHashMap.put(entry.getKey(), entry.getValue());
              });
    } else hashMap = fetchHeaderAuth0authToken();
    return hashMap;
  }

  @SneakyThrows
  default String deserializePath(String path) {
    String[] split = path.split(STR_PATH_SLASH_MARKER);
    String finalStr =
        Arrays.stream(split)
            .map(
                o ->
                    o.split(STR_EQUAL_MARKER)[0]
                        .replace(STR_DATAPROVIDER_MARKER, "")
                        .replace(STR_EQUAL_MARKER, "")
                        .replace(STR_PATH_PARAM_MARKER, ""))
            .collect(Collectors.joining());
    return Arrays.stream(EnDelphiApiV3_EndPointItems.values())
        .filter(
            item ->
                item.name()
                    .equals(CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, finalStr)))
        .findFirst()
        .orElseThrow(
            () ->
                new Exception(
                    String.format(
                        "%s not present in src/test/java/io/delphi/qa/auto/awsmfrw/common_baby/delphi/api/EnDelphiApiV3_EndPointItems(EnDelphiApiV3_EndPointItems.java:5).",
                        CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, finalStr))))
        .get();
  }

  default HashMap<String, String> deserializePathParam(
      String pathParams, Object[] dataProviderParams) {
    int index = 0;
    if (pathParams != null) {
      if (pathParams.contains(STR_PATH_PARAM_MARKER)) {
        HashMap<String, String> map = new HashMap<>();
        String pathItem =
            pathParams.split(STR_PATH_SLASH_MARKER)[0].split(STR_PATH_PARAM_MARKER)[1];
        String[] pathParamSplit = pathItem.split(STR_EQUAL_MARKER);
        String pathParamValue;
        if (pathParamSplit.length > 1) {
          if (pathParamSplit[1].startsWith(STR_DATAPROVIDER_MARKER)) {
            pathParamValue = String.valueOf(dataProviderParams[index]);
            ++index;
          } else {
            pathParamValue =
                findParam(
                    CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, pathParamSplit[1]),
                    this.getClass());
          }

        } else {
          pathParamValue =
              findParam(
                  CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, pathParamSplit[0]),
                  this.getClass());
        }
        map.put(
            CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, pathParamSplit[0]),
            pathParamValue);
        return map;
      } else return null;
    } else return null;
  }

  default HashMap<String, Object> deserializeQueryParams(
      String queryParams, Object[] dataProviderParams) {
    if (queryParams != null) {
      String[] splitMethodNameToParamsArray =
          Arrays.stream(queryParams.split(STR_UNDERSCORE_MARKER))
              .filter(o -> !o.equals(""))
              .toArray(String[]::new);
      if (splitMethodNameToParamsArray.length > 0) {
        return deserializeQueryParamsToMap(splitMethodNameToParamsArray, dataProviderParams);
      }
    }
    return new HashMap<>();
  }

  default HashMap<String, Object> deserializeQueryParamsToMap(
      String[] splitMethodNameToParamsArray, Object[] dataProviderParams) {
    final int[] index = {0};
    HashMap<String, Object> queryParamsMap = new HashMap<>();
    Arrays.stream(splitMethodNameToParamsArray)
        .forEach(
            o -> {
              String[] splitQueryParamItem = o.split(STR_EQUAL_MARKER);
              String queryParamName =
                  CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, splitQueryParamItem[0]);
              String queryParamValue = splitQueryParamItem[0];
              switch (splitQueryParamItem[0]) {
                default:
                  if (splitQueryParamItem.length > 1) {
                    // query param is data provided
                    // name should always starts with 'Arg' prefix
                    if (splitQueryParamItem[1].startsWith("Arg")) {
                      queryParamsMap.put(queryParamName, dataProviderParams[index[0]]);
                      ++index[0];
                    } else {
                      // query param is class member
                      String[] splitSeparatorQueryParamValue =
                          splitQueryParamItem[1].split(STR_SEPARATOR_MARKER);
                      if (splitSeparatorQueryParamValue.length > 1) {
                        queryParamValue =
                            Arrays.stream(splitSeparatorQueryParamValue)
                                .map(String::toLowerCase)
                                .collect(Collectors.joining(","));
                      } else {
                        String to =
                            CaseFormat.UPPER_CAMEL.to(
                                CaseFormat.UPPER_UNDERSCORE, splitQueryParamItem[1]);
                        queryParamValue = findParam(to, this.getClass());
                      }
                      queryParamsMap.put(queryParamName, queryParamValue);
                    }
                  } else {
                    queryParamsMap.put(
                        queryParamName,
                        findParam(
                            CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, queryParamValue),
                            this.getClass()));
                  }
                  break;
              }
            });
    return queryParamsMap;
  }

  default <E extends Enum<E>> E chooseFromEnum(E[] enumItems, String entryItem) {
    return Arrays.stream(enumItems)
        .filter(
            enumItem -> {
              String prefix = CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, entryItem);
              return enumItem.name().startsWith(prefix);
            })
        .findFirst()
        .orElseThrow(FeatureNotImplementedException::new);
  }

  default String deserializeRestMethodName(String restMethodName) {
    return restMethodName.toLowerCase(Locale.ROOT);
  }

  /* Implement all:
  Request method:
  Request URI:
  Proxy:
  Request params:
  Query params:
  Form params:
  Path params:
  Headers:
  Cookies:
  Multiparts:
  Body:
   * */
  default HashMap<String, String> tokenizeMethodName(String methodName) {
    HashMap<String, String> restTestMap = new HashMap<>();
    String[] tokenized = tokenize(methodName, STR_BODY_MARKER);
    restTestMap.put("body", tokenized[1]);
    tokenized = tokenize(tokenized[0], STR_QUERY_MARKER);
    restTestMap.put("query", tokenized[1]);
    tokenized = tokenize(tokenized[0], STR_PATH_MARKER);
    restTestMap.put("path", tokenized[1]);
    tokenized = tokenize(tokenized[0], STR_HEADER_MARKER);
    restTestMap.put("header", tokenized[1]);
    tokenized = tokenize(tokenized[0], STR_METHOD_MARKER);
    restTestMap.put("method", tokenized[1] != null ? tokenized[1] : "get");
    tokenized = tokenize(tokenized[0], STR_ASSERT_MARKER);
    restTestMap.put("assert", tokenized[1] != null ? tokenized[1] : "Sc200");
    return restTestMap;
  }

  default String deserializeStatusCode(String method) {
    String statusCodes =
        Arrays.stream(method.split(STR_UNDERSCORE_MARKER))
            .filter(o -> o.startsWith(STR_STATUS_CODE_MARKER))
            .map(o -> o.replace(STR_STATUS_CODE_MARKER, ""))
            .collect(Collectors.toList())
            .get(0);
    return statusCodes;
  }

  default AbstractRestAssuredClient.CustomRequestSpec buildRestRequestSpec(
      String methodName, Object[] dataProviderParams) {
    HashMap<String, String> tokenizedMap = tokenizeMethodName(methodName);
    return new AbstractRestAssuredClient.CustomRequestSpec()
        .withMethod(deserializeRestMethodName(tokenizedMap.get("method")))
        .withHeaders(deserializeHeaders(tokenizedMap.get("header")))
        .withPath(deserializePath(tokenizedMap.get("path")))
        .withBodies(deserializeBodies(tokenizedMap.get("body")))
        .withPathParam(deserializePathParam(tokenizedMap.get("path"), dataProviderParams))
        .withQueryParams(deserializeQueryParams(tokenizedMap.get("query"), dataProviderParams))
        .withStatusCodes(deserializeStatusCode(tokenizedMap.get("assert")));
  }

  default List<String> deserializeBodies(String body) {
    if (body != null) {
      return Collections.singletonList(
          findParam(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, body), this.getClass()));
    }
    return null;
  }

  @SneakyThrows
  default String findParam(String fieldNameExactAsDeclaredInClass, Class<?> aClass) {
    final String[] fieldString = {DEFAULT_FIELD};
    fieldString[0] = fieldValue(fieldNameExactAsDeclaredInClass, aClass);
    while (fieldString[0].equals(DEFAULT_FIELD) && aClass != null) {
      fieldString[0] = getFieldValue(fieldNameExactAsDeclaredInClass, aClass);
      aClass = aClass.getSuperclass();
    }
    if (fieldString[0].equals(DEFAULT_FIELD))
      throw new FeatureNotImplementedException(
          String.format(
              "\n\texception: %s"
                  + "\n\tproblem: %s - query param is not implemented."
                  + "\n\tsolution: 'String %s = %s%s%s;' should be added.",
              new NoSuchFieldException(),
              fieldNameExactAsDeclaredInClass,
              fieldNameExactAsDeclaredInClass,
              "\"",
              fieldNameExactAsDeclaredInClass.toLowerCase(),
              "\""));
    return fieldString[0];
  }

  default String getFieldValue(String fieldNameExactAsDeclaredInClass, Class<?> aClass) {
    final String[] fieldString = {DEFAULT_FIELD};
    Iterator<Class<?>> iterator =
        Arrays.stream(Objects.requireNonNull(aClass).getInterfaces()).iterator();
    while (fieldString[0].equals(DEFAULT_FIELD) && iterator.hasNext())
      fieldString[0] = filterInterfaces(fieldNameExactAsDeclaredInClass, iterator.next());
    return fieldString[0].equals(DEFAULT_FIELD)
        ? fieldValue(fieldNameExactAsDeclaredInClass, aClass)
        : fieldString[0];
  }

  default String filterInterfaces(String fieldNameExactAsDeclaredInClass, Class<?> aClass) {
    final String[] result = {null};
    result[0] = fieldValue(fieldNameExactAsDeclaredInClass, aClass);
    Iterator<Class<?>> iterator = Arrays.stream(aClass.getInterfaces()).iterator();
    if (result[0].equals(DEFAULT_FIELD) && iterator.hasNext()) {
      result[0] = filterInterfaces(fieldNameExactAsDeclaredInClass, iterator.next());
    }
    return result[0];
  }

  default String fieldValue(String fieldNameExactAsDeclaredInClass, Class<?> i) {
    synchronized (String.class) {
      try {
        Field declaredField = i.getDeclaredField(fieldNameExactAsDeclaredInClass);
        declaredField.setAccessible(true);
        return declaredField.get(this).toString();
      } catch (NoSuchFieldException e) {
        log().info("NoSuchFieldException: " + e);
      } catch (IllegalAccessException e) {
        log().info("IllegalAccessException: " + e);
      }
    }
    return DEFAULT_FIELD;
  }

  @SneakyThrows
  default <A> boolean isAnnotationExists(Annotation[] annotations, Class<A> annotationClass) {
    return Arrays.stream(annotations).anyMatch(o -> o.annotationType().equals(annotationClass));
  }

  default String[] tokenize(String incomeText, String marker) {
    if (incomeText.contains(marker)) {
      String[] tokens = new String[0];
      try {
        tokens = incomeText.split(marker);
      } catch (ArrayIndexOutOfBoundsException e) {
        IsaLogging.log(this.getClass(), String.format("%s request item not specified.", marker));
      }
      return tokens.length > 1 ? tokens : new String[] {tokens[0], STR_EMPTY_TOKEN};
    } else return new String[] {incomeText, STR_EMPTY_TOKEN};
  }
}
