package vn.azteam.tpf.service;

import com.codahale.metrics.annotation.Timed;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.elasticsearch.ResourceAlreadyExistsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vn.azteam.tpf.domain.*;
import vn.azteam.tpf.repository.*;
import vn.azteam.tpf.repository.search.*;

import javax.persistence.ManyToMany;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

@Service
@Transactional(readOnly = true)
public class ElasticsearchIndexService {

    private static final Lock reindexLock = new ReentrantLock();

    private final Logger log = LoggerFactory.getLogger(ElasticsearchIndexService.class);

    private final TBDetailUserRepository tbDetailUserRepository;
    private final TBDetailUserSearchRepository tbDetailUserSearchRepository;

    private final TBPermissionRepository tbPermissionRepository;
    private final TBPermissionSearchRepository tbPermissionSearchRepository;

    private final TBFunctionRepository tbFunctionRepository;
    private final TBFunctionSearchRepository tbFunctionSearchRepository;

    private final TBRoleRepository tbRoleRepository;
    private final TBRoleSearchRepository tbRoleSearchRepository;

    private final UserRepository userRepository;

    private final UserSearchRepository userSearchRepository;

    private final ElasticsearchOperations elasticsearchTemplate;

    public ElasticsearchIndexService(
        TBDetailUserRepository tbDetailUserRepository,
        TBDetailUserSearchRepository tbDetailUserSearchRepository,

        TBPermissionRepository tbPermissionRepository,
        TBPermissionSearchRepository tbPermissionSearchRepository,
        TBFunctionRepository tbFunctionRepository,
        TBFunctionSearchRepository tbFunctionSearchRepository,

        TBRoleRepository tbRoleRepository,
        TBRoleSearchRepository tbRoleSearchRepository,
        UserRepository userRepository,
        UserSearchRepository userSearchRepository,
        ElasticsearchOperations elasticsearchTemplate) {
        this.tbDetailUserRepository = tbDetailUserRepository;
        this.tbDetailUserSearchRepository = tbDetailUserSearchRepository;
        this.tbPermissionRepository = tbPermissionRepository;
        this.tbPermissionSearchRepository = tbPermissionSearchRepository;
        this.tbFunctionRepository = tbFunctionRepository;
        this.tbFunctionSearchRepository = tbFunctionSearchRepository;
        this.tbRoleRepository = tbRoleRepository;
        this.tbRoleSearchRepository = tbRoleSearchRepository;
        this.userRepository = userRepository;
        this.userSearchRepository = userSearchRepository;
        this.elasticsearchTemplate = elasticsearchTemplate;
    }

    @Async
    @Timed
    public void reindexAll() {
        if (reindexLock.tryLock()) {
            try {
                reindexForClass(TBDetailUser.class, tbDetailUserRepository, tbDetailUserSearchRepository);
                reindexForClass(TBPermission.class, tbPermissionRepository, tbPermissionSearchRepository);
                reindexForClass(TBFunction.class, tbFunctionRepository, tbFunctionSearchRepository);
                reindexForClass(TBRole.class, tbRoleRepository, tbRoleSearchRepository);
                reindexForClass(User.class, userRepository, userSearchRepository);

                log.info("Elasticsearch: Successfully performed reindexing");
            } finally {
                reindexLock.unlock();
            }
        } else {
            log.info("Elasticsearch: concurrent reindexing attempt");
        }
    }

    @SuppressWarnings("unchecked")
    private <T, ID extends Serializable> void reindexForClass(Class<T> entityClass, JpaRepository<T, ID> jpaRepository,
                                                              ElasticsearchRepository<T, ID> elasticsearchRepository) {
        elasticsearchTemplate.deleteIndex(entityClass);
        try {
            elasticsearchTemplate.createIndex(entityClass);
        } catch (ResourceAlreadyExistsException e) {
            // Do nothing. Index was already concurrently recreated by some other service.
        }
        elasticsearchTemplate.putMapping(entityClass);
        if (jpaRepository.count() > 0) {
            // if a JHipster entity field is the owner side of a many-to-many relationship, it should be loaded manually
            List<Method> relationshipGetters = Arrays.stream(entityClass.getDeclaredFields())
                .filter(field -> field.getType().equals(Set.class))
                .filter(field -> field.getAnnotation(ManyToMany.class) != null)
                .filter(field -> field.getAnnotation(ManyToMany.class).mappedBy().isEmpty())
                .filter(field -> field.getAnnotation(JsonIgnore.class) == null)
                .map(field -> {
                    try {
                        return new PropertyDescriptor(field.getName(), entityClass).getReadMethod();
                    } catch (IntrospectionException e) {
                        log.error("Error retrieving getter for class {}, field {}. Field will NOT be indexed",
                            entityClass.getSimpleName(), field.getName(), e);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

            int size = 100;
            for (int i = 0; i <= jpaRepository.count() / size; i++) {
                Pageable page = PageRequest.of(i, size);
                log.info("Indexing page {} of {}, size {}", i, jpaRepository.count() / size, size);
                Page<T> results = jpaRepository.findAll(page);
                results.map(result -> {
                    // if there are any relationships to load, do it now
                    relationshipGetters.forEach(method -> {
                        try {
                            // eagerly load the relationship set
                            ((Set) method.invoke(result)).size();
                        } catch (Exception ex) {
                            log.error(ex.getMessage());
                        }
                    });
                    return result;
                });
                elasticsearchRepository.saveAll(results.getContent());
            }
        }
        log.info("Elasticsearch: Indexed all rows for {}", entityClass.getSimpleName());
    }
}
