package tv.pluto.grpc;

import static org.testng.Assert.fail;

import com.plutotv.test.lib.LogUtils;
import io.grpc.ClientInterceptor;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.assertj.core.util.Streams;
import tv.pluto.utils.Route;

public abstract class GenericGrpcClient {
  protected static final ClientInterceptor CLIENT_INTERCEPTOR =
      MetadataUtils.newAttachHeadersInterceptor(getMetadata());

  protected static final ClientInterceptor CLIENT_INTERCEPTOR_AUTH =
      MetadataUtils.newAttachHeadersInterceptor(getMetadataAuth());

  protected com.google.protobuf.MessageOrBuilder getDocumentsUpdatedBetweenDatesRequestBuilder;
  protected com.google.protobuf.MessageOrBuilder getCFeedsAllIdsBuilder;
  protected com.google.protobuf.MessageOrBuilder getCFeedsSyncDocumentsBuilder;

  private static Metadata getMetadata() {
    // create a custom header
    Metadata header = new Metadata();
    Metadata.Key<String> key = Metadata.Key.of("app_name", Metadata.ASCII_STRING_MARSHALLER);
    header.put(key, "automation_tests");

    return header;
  }

  private static Metadata getMetadataAuth() {
    // create a custom header
    Metadata header = new Metadata();
    Metadata.Key<String> appName = Metadata.Key.of("app_name", Metadata.ASCII_STRING_MARSHALLER);
    header.put(appName, "automation_tests");

    String authToken = Route.getDecodedAuthToken();

    if (authToken.isEmpty()) {
      fail("No authToken property for the Metadata");
    }

    Metadata.Key<String> authTokenHeader =
        Metadata.Key.of("auth_token", Metadata.ASCII_STRING_MARSHALLER);
    header.put(authTokenHeader, authToken);

    return header;
  }

  @SuppressWarnings("unchecked")
  private synchronized <T> Iterator<T> getUpdates(
      String updatedAtMin, String updatedAtMax, String grpcMethod) {

    Object builder = getDocumentsUpdatedBetweenDatesRequestBuilder;
    Object blockingStub = null;
    Field blockingStubField;

    try {
      blockingStubField = this.getClass().getDeclaredField("blockingStub");
      blockingStubField.setAccessible(true);
      Class<?> blockingStubFieldType = blockingStubField.getType();
      blockingStub = blockingStubFieldType.cast(blockingStubField.get(this));
    } catch (NoSuchFieldException | IllegalAccessException e) {
      LogUtils.logSevereMessageThenFail(e.getLocalizedMessage());
    }

    Method[] methods = getDocumentsUpdatedBetweenDatesRequestBuilder.getClass().getMethods();

    Method[] methodsBlockingStub = blockingStub.getClass().getMethods();

    Method getForContentFeeds = buildMethod(methodsBlockingStub, grpcMethod);
    Method setUpdatedAtMax = buildMethod(methods, "setUpdatedAtMax");
    Method setUpdatedAtMin = buildMethod(methods, "setUpdatedAtMin");
    Method buildMethod = buildMethod(methods, "build");

    Object request;

    Iterator<T> result = null;
    try {
      builder = Objects.requireNonNull(setUpdatedAtMin).invoke(builder, updatedAtMin);
      builder = Objects.requireNonNull(setUpdatedAtMax).invoke(builder, updatedAtMax);

      request = Objects.requireNonNull(buildMethod).invoke(builder);

      result =
          (Iterator<T>) Objects.requireNonNull(getForContentFeeds).invoke(blockingStub, request);
    } catch (IllegalAccessException | InvocationTargetException e) {
      LogUtils.logSevereMessageThenFail(e.getLocalizedMessage());
    }

    return result;
  }

  public synchronized <T> Iterator<T> getCFeedsResponse(
      Boolean vod, String lastRunAt, Boolean isPublished, String grpcMethod) {
    Object blockingStub = null;
    Field blockingStubField;

    try {
      blockingStubField = this.getClass().getDeclaredField("blockingStub");
      blockingStubField.setAccessible(true);
      Class<?> blockingStubFieldType = blockingStubField.getType();
      blockingStub = blockingStubFieldType.cast(blockingStubField.get(this));
    } catch (NoSuchFieldException | IllegalAccessException e) {
      LogUtils.logSevereMessageThenFail(e.getLocalizedMessage());
    }

    Method[] methods = null;
    if (grpcMethod.contains("FeedsSyncAllIds"))
      methods = getCFeedsAllIdsBuilder.getClass().getMethods();
    else methods = getCFeedsSyncDocumentsBuilder.getClass().getMethods();

    Method[] methodsBlockingStub = blockingStub.getClass().getMethods();

    Method getForContentFeeds = buildMethod(methodsBlockingStub, grpcMethod);
    Method buildMethod = buildMethod(methods, "build");

    Object request = null;
    Iterator<T> result = null;
    try {
      if (lastRunAt != null && isPublished != null) {
        Object builder = getCFeedsSyncDocumentsBuilder;
        Method lasRunMethod = buildMethod(methods, "setLastRunAt");
        Method published = buildMethod(methods, "setPublished");
        Method methodVod = buildMethod(methods, "setVod");
        builder = Objects.requireNonNull(published).invoke(builder, isPublished);
        builder = Objects.requireNonNull(lasRunMethod).invoke(builder, lastRunAt);
        builder = Objects.requireNonNull(methodVod).invoke(builder, vod);
        request = Objects.requireNonNull(buildMethod).invoke(builder);
      } else if (lastRunAt == null && isPublished == null) {
        request = Objects.requireNonNull(buildMethod).invoke(getCFeedsAllIdsBuilder);
      } else {
        if (lastRunAt != null) {
          Object builder = getCFeedsSyncDocumentsBuilder;
          Method lasRunMethod = buildMethod(methods, "setLastRunAt");
          builder = Objects.requireNonNull(lasRunMethod).invoke(builder, lastRunAt);
          request = Objects.requireNonNull(buildMethod).invoke(builder);
        }
        if (isPublished != null) {
          Object builder = getCFeedsAllIdsBuilder;
          Method published = buildMethod(methods, "setPublished");
          builder = Objects.requireNonNull(published).invoke(builder, isPublished);
          request = Objects.requireNonNull(buildMethod).invoke(builder);
        }
      }

      result =
          (Iterator<T>) Objects.requireNonNull(getForContentFeeds).invoke(blockingStub, request);
    } catch (IllegalAccessException | InvocationTargetException e) {
      LogUtils.logSevereMessageThenFail(e.getLocalizedMessage());
    }
    return result;
  }

  public synchronized <T> Iterator<T> getDocumentsUpdatedBetweenDates(
      String updatedAtMin, String updatedAtMax) {
    LogUtils.logInfoMessage(
        "Getting "
            + this.getClass().getSimpleName()
            + " updates between dates updatedAtMin="
            + updatedAtMin
            + " updatedAtMax="
            + updatedAtMax);

    return getUpdates(updatedAtMin, updatedAtMax, "getDocumentsUpdatedBetweenDates");
  }

  private Method buildMethod(Method[] methods, String methodName) {
    return Arrays.stream(methods)
        .filter(method -> method.getName().equals(methodName))
        .findFirst()
        .orElse(null);
  }

  private synchronized <T, U> T getAllPagedInternal(
      Integer limit,
      Integer offset,
      List<U> sort,
      List filters,
      Boolean totalCount,
      boolean enableRegex,
      List<String> listProjections)
      throws NoSuchFieldException, IllegalAccessException, InvocationTargetException {
    Object builder;
    Object blockingStub;
    Field builderField;
    Field blockingStubField;

    builderField = this.getClass().getDeclaredField("getAllPagedBuilder");
    builderField.setAccessible(true);
    Class<?> builderFieldType = builderField.getType();
    builder = builderFieldType.cast(builderField.get(this));

    blockingStubField = this.getClass().getDeclaredField("blockingStub");
    blockingStubField.setAccessible(true);
    Class<?> blockingStubFieldType = blockingStubField.getType();
    blockingStub = blockingStubFieldType.cast(blockingStubField.get(this));

    Method[] methods = builder.getClass().getMethods();
    Method[] methodsBlockingStub = blockingStub.getClass().getMethods();

    Method getForContentFeeds = buildMethod(methodsBlockingStub, "getAllPaged");
    Method setLimit = buildMethod(methods, "setLimit");
    Method setOffset = buildMethod(methods, "setOffset");
    Method addAllSort = buildMethod(methods, "addAllSort");
    Method buildMethod = buildMethod(methods, "build");
    Method filtersMethod = buildMethod(methods, "addAllFilters");
    Method disableTotalCountMethod = buildMethod(methods, "setDisableTotalCount");
    Method enableRegexMethod = buildMethod(methods, "setEnableRegex");
    Method listProjectionsMethod = buildMethod(methods, "addAllProjection");

    Object request;
    T result;

    if (limit != null) {
      builder = Objects.requireNonNull(setLimit).invoke(builder, limit);
    }

    if (offset != null) {
      builder = Objects.requireNonNull(setOffset).invoke(builder, offset);
    }

    if (sort != null) {
      builder = Objects.requireNonNull(addAllSort).invoke(builder, sort);
    }

    if (filters != null) {
      builder = Objects.requireNonNull(filtersMethod).invoke(builder, filters);
    }

    if (listProjections != null) {
      builder = Objects.requireNonNull(listProjectionsMethod).invoke(builder, listProjections);
    }

    if (totalCount != null) {
      builder = Objects.requireNonNull(disableTotalCountMethod).invoke(builder, totalCount);
    }

    // Clips does not have the enableRegex method
    if (!builderField.getType().getName().contains(".Clips$")) {
      builder = Objects.requireNonNull(enableRegexMethod).invoke(builder, enableRegex);
    }

    request = Objects.requireNonNull(buildMethod).invoke(builder);
    result = (T) Objects.requireNonNull(getForContentFeeds).invoke(blockingStub, request);

    return result;
  }

  public synchronized <T, U> T getAllPagedRegex(
      Integer limit, Integer offset, List<U> sort, List filters, Boolean totalCount) {
    try {
      return getAllPagedInternal(limit, offset, sort, filters, totalCount, true, null);
    } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  public synchronized <T, U> T getAllPaged(
      Integer limit, Integer offset, List<U> sort, List filters, Boolean totalCount) {
    try {
      return getAllPagedInternal(limit, offset, sort, filters, totalCount, false, null);
    } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  public synchronized <T, U> T getAllPagedException(
      Integer limit, Integer offset, List<U> sort, List filters, Boolean totalCount)
      throws NoSuchFieldException, IllegalAccessException, InvocationTargetException {
    return getAllPagedInternal(limit, offset, sort, filters, totalCount, false, null);
  }

  public synchronized <T, U> T getAllPaged(
      Integer limit,
      Integer offset,
      List<U> sort,
      List filters,
      Boolean totalCount,
      List<String> listProjections) {
    try {
      return getAllPagedInternal(limit, offset, sort, filters, totalCount, false, listProjections);
    } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  public List listFromIteratorLimit(Iterator iterator, int limit) {
    return (List) Streams.stream(iterator).limit(limit).collect(Collectors.toList());
  }
}
