package cn.lihongjie.coal.pojoProcessor;

import cn.lihongjie.coal.base.dto.R;
import cn.lihongjie.coal.dictionary.service.DictionaryService;
import cn.lihongjie.coal.organization.service.OrganizationService;
import cn.lihongjie.coal.user.service.UserService;

import io.vavr.Tuple2;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

@Service
@Slf4j
public class PojoProcessor {

    @Autowired UserService userService;

    @Autowired OrganizationService organizationService;

    @Autowired DictionaryService dictionaryService;

    @SneakyThrows
    public void process(Object pojo, boolean logStopWatch) {

        new PojoProcessorCtx(pojo, logStopWatch).process();
    }

    public class PojoProcessorCtx {

        private final Map<String, Consumer<String>> userNameActions = new HashMap<>();
        private final Map<String, Consumer<String>> organizationNameActions = new HashMap<>();
        private final Map<Tuple2<String, String>, Consumer<String>> dictActions = new HashMap<>();
        private final boolean logStopWatch;
        private StopWatch stopWatch;
        private final Object pojo;
        private final Set<Object> seen = new HashSet<>();

        private final Map<Class, List<Field>> allFieldsMap = new HashMap<>();

        public PojoProcessorCtx(Object pojo) {
            this(pojo, false);
        }

        public PojoProcessorCtx(Object pojo, boolean logStopWatch) {
            this.pojo = pojo;
            this.logStopWatch = logStopWatch;
        }

        private static boolean isJdkClass(Class<?> cls) {
            return cls.getName().startsWith("java.");
        }

        public static boolean isContainer(Class<?> cls) {
            return cls.isArray()
                    || Iterable.class.isAssignableFrom(cls)
                    || Map.class.isAssignableFrom(cls)
                    || Optional.class.isAssignableFrom(cls)
                    || R.class.isAssignableFrom(cls);
        }

        @SneakyThrows
        public void process() {

            if (this.pojo == null) {
                return;
            }
            stopWatch = new StopWatch("process pojo " + pojo.getClass().getName());

            stopWatch.start("collect actions");
            collectActions(this.pojo);
            stopWatch.stop();

            doActions();

            if (logStopWatch) {

                log.info(stopWatch.prettyPrint());
            }
        }

        @SneakyThrows
        private void collectActions(Object pojo) {

            if (pojo == null) {
                return;
            }

            if (pojo instanceof Iterable<?> it) {
                it.forEach(this::collectActions);
                return;
            } else if (pojo instanceof Map<?, ?> map) {
                map.values().forEach(this::collectActions);
                return;
            } else if (pojo instanceof Optional<?> optional) {
                optional.ifPresent(this::collectActions);
                return;
            } else if (pojo instanceof Object[] array) {
                Arrays.stream(array).forEach(this::collectActions);
                return;
            } else if (pojo instanceof R<?> r) {
                collectActions(r.getData());
                return;
            }

            Class<?> cls = pojo.getClass();
            // process pojo

            // check if base java type, use package name
            if (isJdkClass(cls)) {
                return;
            }

            if (!seen.add(ObjectUtils.identityToString(pojo))) {
                return;
            }

            List<Field> allFields = allFieldsMap.computeIfAbsent(cls, FieldUtils::getAllFieldsList);

            Map<String, List<Field>> fieldNameMap =
                    allFields.stream().collect(Collectors.groupingBy(Field::getName));

            for (Field field : allFields) {
                if (Modifier.isStatic(field.getModifiers())) {
                    continue;
                }
                if (field.isAnnotationPresent(UserName.class)
                        || field.isAnnotationPresent(OrganizationName.class)) {

                    UserName userName = field.getAnnotation(UserName.class);
                    OrganizationName organizationName = field.getAnnotation(OrganizationName.class);

                    String ref = userName == null ? organizationName.ref() : userName.ref();

                    Field refField = null;

                    if (ref.equalsIgnoreCase("#auto")) {

                        if (field.getName().endsWith("Name")) {
                            String refName =
                                    field.getName().substring(0, field.getName().length() - 4)
                                            + "Id";
                            if (!fieldNameMap.containsKey(refName)) {
                                throw new RuntimeException(
                                        "ref field not found  " + field.getName() + " " + ref);
                            }

                            refField = fieldNameMap.get(refName).get(0);
                        }

                    } else {

                        if (!fieldNameMap.containsKey("ref")) {
                            throw new RuntimeException(
                                    "ref field not found  " + field.getName() + " " + ref);
                        }

                        refField = fieldNameMap.get("ref").get(0);
                    }

                    Object val = FieldUtils.readField(refField, pojo, true);

                    if (val != null) {

                        (userName == null ? organizationNameActions : userNameActions)
                                .merge(
                                        val.toString(),
                                        (String name) -> {
                                            try {
                                                FieldUtils.writeField(field, pojo, name, true);
                                            } catch (IllegalAccessException e) {
                                                throw new RuntimeException(e);
                                            }
                                        },
                                        Consumer::andThen);
                    }
                } else if (field.isAnnotationPresent(DictTranslate.class)) {

                    DictTranslate dictTranslate = field.getAnnotation(DictTranslate.class);
                    String dictKey = dictTranslate.dictKey();

                    String ref = dictTranslate.ref();
                    Field refField = null;
                    if (ref.equalsIgnoreCase("#auto")) {

                        if (field.getName().endsWith("Name")) {
                            String refName =
                                    field.getName().substring(0, field.getName().length() - 4);
                            if (!fieldNameMap.containsKey(refName)) {
                                throw new RuntimeException(
                                        "ref field not found  " + field.getName() + " " + ref);
                            }

                            refField = fieldNameMap.get(refName).get(0);
                        }

                    } else {

                        if (!fieldNameMap.containsKey("ref")) {
                            throw new RuntimeException(
                                    "ref field not found  " + field.getName() + " " + ref);
                        }

                        refField = fieldNameMap.get("ref").get(0);
                    }




                    Object val = FieldUtils.readField(refField, pojo, true);

                    String dictCode = val == null ? "" : val.toString();

                    if (StringUtils.isNotEmpty(dictCode)) {
                        dictActions.merge(
                                new Tuple2<>(dictKey, dictCode),
                                (String name) -> {
                                    try {
                                        FieldUtils.writeField(field, pojo, name, true);
                                    } catch (IllegalAccessException e) {
                                        throw new RuntimeException(e);
                                    }
                                },
                                Consumer::andThen);
                    }

                } else {

                    if (isContainer(field.getType()) || !isJdkClass(field.getType())) {

                        Object val = FieldUtils.readField(field, pojo, true);
                        collectActions(val);
                    }
                }
            }
        }

        private void doActions() {

            if (MapUtils.isNotEmpty(userNameActions)) {

                stopWatch.start("userService.getIdNameMap  " + userNameActions.size());
                Map<String, String> userIdNameMap =
                        userService.getIdNameMap(userNameActions.keySet());

                stopWatch.stop();

                stopWatch.start("do userNameActions " + userNameActions.size());
                userNameActions.forEach(
                        (k, v) -> {
                            v.accept(userIdNameMap.get(k));
                        });

                stopWatch.stop();
            }

            if (MapUtils.isNotEmpty(organizationNameActions)) {
                stopWatch.start(
                        "organizationService.getIdNameMap  " + organizationNameActions.size());
                Map<String, String> organizationIdNameMap =
                        organizationService.getIdNameMap(organizationNameActions.keySet());

                stopWatch.stop();

                stopWatch.start("do organizationNameActions " + organizationNameActions.size());
                organizationNameActions.forEach(
                        (k, v) -> {
                            v.accept(organizationIdNameMap.get(k));
                        });

                stopWatch.stop();
            }

            if (MapUtils.isNotEmpty(dictActions)) {
                stopWatch.start("dictionaryService.getDictNameMap  " + dictActions.size());

                Map<Tuple2<String, String>, String> dictMap =
                        dictionaryService.getDictNameMap(dictActions.keySet());

                stopWatch.stop();

                stopWatch.start("do dictActions " + dictActions.size());
                dictActions.forEach(
                        (k, v) -> {
                            v.accept(dictMap.get(k));
                        });

                stopWatch.stop();
            }
        }
    }
}
