package com.airlock.iam.base.infrastructure.reflection;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import jakarta.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;

public final class ReflectionHelper {
   public static <T> T getFieldValue(Object obj, String fieldPath) {
      Object currentObject = obj;
      String[] fieldNames = processPath(fieldPath);
      String currentPath = ".";

      for (String fieldName : fieldNames) {
         List<String> nameParts = Lists.newArrayList(fieldName.split(":"));
         String fieldNamePart = nameParts.getFirst();
         Optional<Field> optional = getField(fieldNamePart, currentObject.getClass());
         if (optional.isEmpty()) {
            throw new IllegalStateException(
               "Property " + fieldNamePart + " of class " + currentObject.getClass().getSimpleName() + " not found at \"" + currentPath + "\""
            );
         }

         Field field = optional.get();
         makePrivateFieldAccessible(field);

         try {
            currentObject = field.get(currentObject);
            if (nameParts.size() > 1) {
               currentObject = getListOrMapElement(nameParts, currentObject, currentPath);
            }
         } catch (IllegalArgumentException | IllegalAccessException e) {
            throw new IllegalStateException(e);
         }

         currentPath = updatePath(currentPath, fieldName);
      }

      return (T)currentObject;
   }

   public static void setFieldValue(Object obj, String fieldPath, Object value) {
      Object currentObject = obj;
      String[] fieldNames = processPath(fieldPath);
      String currentPath = ".";
      int fieldIndex = 0;

      Field field;
      do {
         List<String> nameParts = Lists.newArrayList(fieldNames[fieldIndex].split(":"));
         String fieldNamePart = nameParts.getFirst();
         Optional<Field> optional = getField(fieldNamePart, currentObject.getClass());
         if (optional.isEmpty()) {
            throw new IllegalStateException(
               "Property " + fieldNamePart + " of class " + currentObject.getClass().getSimpleName() + " not found in \"" + fieldPath + "\""
            );
         }

         field = optional.get();
         makePrivateFieldAccessible(field);

         try {
            if (fieldIndex < fieldNames.length - 1) {
               currentObject = field.get(currentObject);
               if (nameParts.size() > 1) {
                  currentObject = getListOrMapElement(nameParts, currentObject, currentPath);
               }
            }
         } catch (IllegalArgumentException | IllegalAccessException e) {
            throw new IllegalStateException(e);
         }
      } while (++fieldIndex < fieldNames.length);

      try {
         List<String> nameParts = Lists.newArrayList(fieldNames[fieldNames.length - 1].split(":"));
         if (nameParts.size() > 1) {
            Object listObject = field.get(currentObject);
            List<Object> list = (List<Object>)listObject;
            List<Object> newList = Lists.newArrayList(list);
            int listElementIndex = Integer.parseInt(nameParts.get(1));
            String fieldNamePart = nameParts.getFirst();
            if (listElementIndex >= list.size()) {
               throw new IllegalStateException(
                  "List property element at index "
                     + listElementIndex
                     + " exceeds bounds of list property "
                     + fieldNamePart
                     + " with path \""
                     + currentPath
                     + "\""
               );
            }

            newList.set(listElementIndex, value);
            value = newList;
         }

         field.set(currentObject, value);
      } catch (IllegalArgumentException | IllegalAccessException e) {
         throw new IllegalStateException(e);
      }
   }

   private static Object getListOrMapElement(List<String> nameParts, Object currentObject, String currentPath) {
      if (currentObject instanceof List) {
         return getListElement(nameParts, (List<Object>)currentObject, currentPath);
      } else if (currentObject instanceof Map) {
         return getMapElement(nameParts, (Map<String, Object>)currentObject, currentPath);
      } else {
         throw new IllegalStateException("Object is not a list nor a map, but addressed as " + nameParts + " with path \"" + currentPath + "\"");
      }
   }

   private static Object getMapElement(List<String> nameParts, Map<String, Object> map, String currentPath) {
      Object obj = map.get(nameParts.get(1));
      if (obj == null) {
         throw new IllegalStateException(
            "No entry found for key \"" + nameParts.get(1) + "\" at map property " + nameParts.getFirst() + " with path \"" + currentPath + "\""
         );
      } else {
         return obj;
      }
   }

   private static Object getListElement(List<String> nameParts, List<Object> list, String currentPath) {
      int listElementIndex = Integer.parseInt(nameParts.get(1));
      if (listElementIndex >= list.size()) {
         throw new IllegalStateException(
            "List property element at index "
               + listElementIndex
               + " exceeds bounds of list property "
               + nameParts.getFirst()
               + " with path \""
               + currentPath
               + "\""
         );
      } else {
         return list.get(listElementIndex);
      }
   }

   public static Optional<Field> getField(String name, Class<?> cls) {
      return Optional.ofNullable(FieldUtils.getField(cls, name, true));
   }

   @Nullable
   public static String getStringValue(Field field, Object target, String defaultValue) {
      String value = getFieldValue(field, target);
      return value == null ? defaultValue : value;
   }

   public static <T> T getFieldValue(Field field, Object target) {
      try {
         makePrivateFieldAccessible(field);
         return (T)field.get(target);
      } catch (IllegalArgumentException | IllegalAccessException e) {
         throw new IllegalStateException(e);
      }
   }

   private static String[] processPath(String path) {
      failIfPathIsNullOrEmpty(path);
      return path.split("\\.");
   }

   private static void failIfPathIsNullOrEmpty(String path) {
      if (Strings.isNullOrEmpty(path)) {
         throw new IllegalStateException("Invalid plugin path: " + path);
      }
   }

   private static String updatePath(String previousPath, String currentField) {
      return ".".equals(previousPath) ? previousPath + currentField : previousPath + "." + currentField;
   }

   public static Optional<Method> getFirstMethodWithName(Class<?> type, String methodName) {
      return Optional.ofNullable(MethodUtils.getMatchingMethod(type, methodName, new Class[0]));
   }

   public static Object invokeMethod(Object target, String methodName, Object... args) {
      return invokeMethod(target.getClass(), target, methodName, args);
   }

   public static Object invokeMethod(Class<?> clazz, String methodName, Object... args) {
      return invokeMethod(clazz, null, methodName, args);
   }

   private static Object invokeMethod(Class<?> clazz, Object instance, String methodName, Object[] args) {
      Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
      Method method = MethodUtils.getMatchingMethod(clazz, methodName, argTypes);
      if (method == null) {
         throw new IllegalStateException("No method with name " + methodName + " and the following argument types found: " + Arrays.toString(argTypes));
      }

      try {
         return method.invoke(instance, args);
      } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
         throw new IllegalStateException("Could not invoke method with name " + methodName, e);
      }
   }

   public static void makePrivateFieldAccessible(Field field) {
      AccessibleObject.setAccessible(new AccessibleObject[]{field}, true);
   }

   public static <T> T newInstance(Class<T> type) {
      try {
         return type.getConstructor().newInstance();
      } catch (ReflectiveOperationException e) {
         throw new IllegalStateException(e);
      }
   }

   public static Optional<Method> getSingleMethodWithAnnotation(Class<?> type, Class<? extends Annotation> methodAnnotation) {
      Collection<Method> methods = getAllMethdodsWithAnnotation(type, methodAnnotation);
      if (methods.size() > 1) {
         throw new IllegalStateException(String.format("Expected either 0 or 1 method annotated with %s on class %s", methodAnnotation, type));
      } else {
         return methods.stream().findFirst();
      }
   }

   public static Collection<Method> getAllMethdodsWithAnnotation(Class<?> type, Class<? extends Annotation> methodAnnotation) {
      return MethodUtils.getMethodsListWithAnnotation(type, methodAnnotation, true, true);
   }

   public static Set<Field> getAllFields(Class<?> declaringClass) {
      return Set.copyOf(FieldUtils.getAllFieldsList(declaringClass));
   }

   public static Set<Field> getAllFieldsOfType(Class<?> fieldType, Class<?> declaringClass) {
      return FieldUtils.getAllFieldsList(declaringClass).stream().filter(field -> fieldType.equals(field.getType())).collect(Collectors.toSet());
   }

   public static Set<Field> getAllFieldsWithAnnotations(Collection<Class<? extends Annotation>> annotations, Class<?> declaringClass) {
      return FieldUtils.getAllFieldsList(declaringClass)
         .stream()
         .filter(field -> annotations.isEmpty() || hasAnyAnnotation(annotations, field))
         .collect(Collectors.toSet());
   }

   private static boolean hasAnyAnnotation(Collection<Class<? extends Annotation>> annotations, Field field) {
      return Stream.of(field.getAnnotations()).map(Annotation::annotationType).anyMatch(annotations::contains);
   }

   public static Set<Field> getAllFieldsOfTypeWithAnnotations(Class<?> fieldType, Collection<Class<? extends Annotation>> annotations, Class<?> declaringClass) {
      return getAllFieldsWithAnnotations(annotations, declaringClass).stream().filter(field -> fieldType.equals(field.getType())).collect(Collectors.toSet());
   }

   private ReflectionHelper() {
      throw new AssertionError();
   }
}
