package com.arms.egovframework.javaservice.esframework.repository.common;

import com.arms.egovframework.javaservice.esframework.annotation.RecentId;
import com.arms.egovframework.javaservice.esframework.annotation.Recent;
import com.arms.egovframework.javaservice.esframework.esquery.bool.EsBoolQuery;
import lombok.extern.slf4j.Slf4j;
import org.opensearch.data.client.orhlc.NativeSearchQuery;
import org.opensearch.data.client.orhlc.NativeSearchQueryBuilder;
import org.opensearch.data.core.OpenSearchOperations;
import org.opensearch.index.query.BoolQueryBuilder;
import org.opensearch.index.query.QueryBuilders;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Query;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.arms.egovframework.javaservice.esframework.util.ReflectionUtil.*;
import static java.util.stream.Collectors.*;

@Slf4j
class RecentFieldConvertor<S,T> {

    private final Map<Object, List<SearchHit<T>>> searchHitsMapList;

    private final ElasticsearchOperations openSearchOperations;

    private final List<? extends T> entities;

    public RecentFieldConvertor(Iterable<S> entities, Class<T> tClass, ElasticsearchOperations openSearchOperations) throws  IllegalStateException{

        this.entities = StreamSupport.stream(entities.spliterator(), false).map(tClass::cast).collect(toList());

        if(this.entities.isEmpty()){
            throw new IllegalStateException("저장할 데이터가 없습니다.");
        }

        String recentIdFieldName = Optional.of(fieldInfo(this.entities, RecentId.class).getAnnotation(Field.class).name())
            .filter(s->!s.isEmpty())
            .orElse(fieldInfo(this.entities, RecentId.class).getName());

        String recentFieldName = Optional.of(fieldInfo(this.entities, Recent.class).getAnnotation(Field.class).name())
            .filter(s->!s.isEmpty())
            .orElse(fieldInfo(this.entities, Recent.class).getName());

        this.openSearchOperations = openSearchOperations;

        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.boolQuery()
                    .filter(QueryBuilders.termsQuery(recentIdFieldName, fieldValues(this.entities, RecentId.class)))
                    .filter(QueryBuilders.termQuery(recentFieldName, true)))
            .build();

        this.searchHitsMapList = Optional.ofNullable(this.search(searchQuery, tClass))
            .map(schHits ->
                schHits.getSearchHits().stream()
                    .filter(a -> a.getIndex() != null)
                    .sorted(Comparator.comparing(a->this.decodeCustomUUIDAndGetTimeStamp(a.getId()), Comparator.reverseOrder()))
                    .collect(groupingBy(a -> {
                        try {
                            String fieldName = fieldInfo(a.getContent().getClass(), RecentId.class).getName();
                            return getValue(a.getContent(),fieldName);
                        } catch (Exception e) {
                            throw new IllegalArgumentException(e);
                        }
                    }))
            ).orElse(new HashMap<>());
    }

    private SearchHits<T> search(Query query,Class<T> clazz) {
        try{
            return openSearchOperations.search(query, clazz);
        }catch (NoSuchIndexException e){
            String errorMessage = e.getMessage();
            if (errorMessage != null && errorMessage.contains("no such index")) {
                return null;
            }
        }catch (Exception e){
            log.error(e.getMessage());
        }
        return null;
    }

    public S recentTrue(S newEntity){
        try {
            String name = fieldInfo(newEntity.getClass(), RecentId.class).getName();

            Object keyObject = getValue(newEntity, name);

            if(searchHitsMapList!=null && searchHitsMapList.containsKey(keyObject)){

                SearchHit<T> searchHit = searchHitsMapList.get(keyObject)
                    .stream()
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException("값이 비워 있습니다."));

                if(newEntity.equals(searchHit.getContent())){
                    return null;
                }else{
                    String fieldName = fieldInfo(newEntity.getClass(), Recent.class).getName();
                    setValue(newEntity,fieldName,true);
                    return newEntity;
                }

            }else{
                String fieldName = fieldInfo(newEntity.getClass(), Recent.class).getName();
                setValue(newEntity,fieldName,true);
                return newEntity;
            }

        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    private List<SearchHit<T>> recentFalse(Function<Map<Object, List<SearchHit<T>>>,Stream<SearchHit<T>>> function){

        return Optional.ofNullable(searchHitsMapList)
            .map(entitiMap -> function.apply(entitiMap)
                .map(hit -> {
                    try {
                        String fieldName = fieldInfo(hit.getContent().getClass(), Recent.class).getName();
                        setValue(hit.getContent(),fieldName,false);
                        return hit;
                    } catch (Exception e) {
                        throw new IllegalArgumentException(e);
                    }
                }).collect(toList())
            )
            .orElse(null);
    }

    public List<SearchHit<T>> setRecentFalseIfNotEqual(T newEntity){

        Function<Map<Object, List<SearchHit<T>>>,Stream<SearchHit<T>>> function = entityMap -> {
            try {
                String fieldName = fieldInfo(newEntity.getClass(), RecentId.class).getName();
                return entityMap.getOrDefault(getValue(newEntity,fieldName), Collections.emptyList())
                    .stream()
                    .filter(hit -> !newEntity.equals(hit.getContent()));
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };

        return this.recentFalse(function);
    }

    public List<SearchHit<T>> setRecentFalseIfDuplicate(T newEntity){

        Function<Map<Object, List<SearchHit<T>>>,Stream<SearchHit<T>>> function = entityMap-> {
            try {
                String name = fieldInfo(newEntity.getClass(), RecentId.class).getName();
                Object key = getValue(newEntity,name);
                return entityMap.getOrDefault(key, Collections.emptyList())
                    .stream()
                    .filter(hit -> newEntity.equals(hit.getContent()))
                    .skip(1);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };

        return this.recentFalse(function);
    }

    public Map<String, List<T>> listMapGroupByIndex(Function<T, List<SearchHit<T>>> function){
        return entities.stream()
                .filter(Objects::nonNull)
                .map(function)
                .filter(Objects::nonNull)
                .flatMap(searchHits -> searchHits.stream().filter(a->a.getIndex()!=null))
                .collect(groupingBy(SearchHit::getIndex, mapping(SearchHit::getContent, toList())));
    }

    private long decodeCustomUUIDAndGetTimeStamp(String base64uuid) {

        byte[] uuidBytes = Base64.getUrlDecoder().decode(base64uuid);

        if (uuidBytes.length != 15) {
            throw new IllegalArgumentException("Invalid UUID byte array length: " + uuidBytes.length);
        }

        return  (uuidBytes[5] & 0xFFL) << 40 |
                (uuidBytes[4] & 0xFFL) << 32 |
                (uuidBytes[3] & 0xFFL) << 24 |
                (uuidBytes[2] & 0xFFL) << 16 |
                (uuidBytes[12] & 0xFFL) << 8 |
                (uuidBytes[14] & 0xFFL);

    }

}
