package vn.azteam.tpf.service;

import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.JoinType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import springfox.documentation.spring.web.json.Json;
import vn.azteam.tpf.domain.*; // for static metamodels
import io.github.jhipster.service.QueryService;
import io.github.jhipster.service.filter.LongFilter;
import vn.azteam.tpf.Constants.Constants;
import vn.azteam.tpf.domain.TBEntity;
import vn.azteam.tpf.repository.TBEntityRepository;
import vn.azteam.tpf.repository.search.TBEntitySearchRepository;
import vn.azteam.tpf.service.dto.*;
import vn.azteam.tpf.service.mapper.TBEntityMapper;

import static vn.azteam.tpf.Constants.Constants.ENTITY_COLOR.*;
import static vn.azteam.tpf.Constants.Constants.ENUM_CROP_STATUS.STATUS_ARE_ACTIVE;
import static vn.azteam.tpf.Constants.Constants.ENUM_CROP_STATUS.STATUS_FINISHED;

/**
 * Service for executing complex queries for TBEntity entities in the database.
 * The main input is a {@link TBEntityCriteria} which gets converted to {@link Specification},
 * in a way that all the filters must apply.
 * It returns a {@link List} of {@link TBEntityDTO} or a {@link Page} of {@link TBEntityDTO} which fulfills the criteria.
 */
@Service
@Transactional(readOnly = true)
public class TBEntityQueryService extends QueryService<TBEntity> {

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

    private final TBEntityRepository tBEntityRepository;

    private final TBEntityMapper tBEntityMapper;

    private final TBEntitySearchRepository tBEntitySearchRepository;

    private final UserService userService;

    private final EntityManager em;

    public TBEntityQueryService(
        TBEntityRepository tBEntityRepository,
        TBEntityMapper tBEntityMapper,
        TBEntitySearchRepository tBEntitySearchRepository,
        UserService userService,
        EntityManager em) {
        this.tBEntityRepository = tBEntityRepository;
        this.tBEntityMapper = tBEntityMapper;
        this.tBEntitySearchRepository = tBEntitySearchRepository;
        this.userService = userService;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link TBEntityDTO} which matches the criteria from the database
     *
     * @param criteria The object which holds all the filters, which the entities should match.
     * @return the matching entities.
     */
    @Transactional(readOnly = true)
    public List<TBEntityDTO> findByCriteria(TBEntityCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<TBEntity> specification = createSpecification(criteria);
        return tBEntityMapper.toDto(tBEntityRepository.findAll(specification));
    }

    /**
     * Return a {@link Page} of {@link TBEntityDTO} which matches the criteria from the database
     *
     * @param criteria The object which holds all the filters, which the entities should match.
     * @param page     The page, which should be returned.
     * @return the matching entities.
     */
    @Transactional(readOnly = true)
    public Page<TBEntityDTO> findByCriteria(TBEntityCriteria criteria, Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        final Specification<TBEntity> specification = createSpecification(criteria);
        return tBEntityRepository.findAll(specification, page)
            .map(tBEntityMapper::toDto);
    }

    @Transactional(readOnly = true)
    public Page<TBEntityDTO> findWarehouseByCriteria(TBEntityCriteria criteria, Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        LongFilter entityTypeIdFilter = new LongFilter();
        Long entityTypeId = Long.valueOf(Constants.DATA_ENTITY_TYPE_ID.WAREHOUSE.getValue());
        entityTypeIdFilter.setEquals(entityTypeId);
        criteria.setTbEntityTypeId(entityTypeIdFilter);
        final Specification<TBEntity> specification = createSpecification(criteria);
        return tBEntityRepository.findAll(specification, page)
            .map(tBEntityMapper::toDto);
    }

    /**
     * Return the number of matching entities in the database
     *
     * @param criteria The object which holds all the filters, which the entities should match.
     * @return the number of matching entities.
     */
    @Transactional(readOnly = true)
    public long countByCriteria(TBEntityCriteria criteria) {
        log.debug("count by criteria : {}", criteria);
        final Specification<TBEntity> specification = createSpecification(criteria);
        return tBEntityRepository.count(specification);
    }

    /**
     * Function to convert TBEntityCriteria to a {@link Specification}
     */
    private Specification<TBEntity> createSpecification(TBEntityCriteria criteria) {
        Specification<TBEntity> specification = Specification.where(null);
        if (criteria != null) {
            UserDTO currentUser = userService.getCurrentUserDTO().get();

            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), TBEntity_.id));
            }
            if (criteria.getQrCode() != null) {
                specification = specification.and(buildStringSpecification(criteria.getQrCode(), TBEntity_.qrCode));
            }
            if (criteria.getName() != null) {
                specification = specification.and(buildStringSpecification(criteria.getName(), TBEntity_.name));
            }
            if (criteria.getAreaM2() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getAreaM2(), TBEntity_.areaM2));
            }
            if (criteria.getLevel() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getLevel(), TBEntity_.level));
            }
            if (criteria.getStatus() != null) {
                specification = specification.and(buildStringSpecification(criteria.getStatus(), TBEntity_.status));
            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TBEntity_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), TBEntity_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), TBEntity_.deletedDate));
            }
            if (criteria.getParentId() != null) {
                specification = specification.and(buildSpecification(criteria.getParentId(),
                    root -> root.join(TBEntity_.tbEntity, JoinType.LEFT).get(TBEntity_.id)));
            }
            if (criteria.getTbAddressId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbAddressId(),
                    root -> root.join(TBEntity_.tbAddress, JoinType.LEFT).get(TBAddress_.id)));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(TBEntity_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(TBEntity_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(TBEntity_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getTbCustomerId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbCustomerId(),
                    root -> root.join(TBEntity_.tbCustomer, JoinType.LEFT).get(TBCustomer_.id)));
            }
            if (criteria.getTbEntityTypeId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbEntityTypeId(),
                    root -> root.join(TBEntity_.tbEntityType, JoinType.LEFT).get(TBEntityType_.id)));
            }

            if(!userService.isAllCustomerRole(currentUser.getRoleId())) {
                LongFilter customerIdFilter = new LongFilter();
                customerIdFilter.setEquals(currentUser.getCustomerId());
                specification = specification.and(buildSpecification(customerIdFilter,
                root -> root.join("tbCustomer", JoinType.LEFT).get("id")));
            }
        }
        return specification;
    }

    public List<TBDetailUserDTO> getListUsersInChargeOf(TBEntityDTO tbEntityDTO, List<TBCropDTO> cropDTOS) {
        List<TBDetailUserDTO> detailUserDTOS = new ArrayList<>();

        List<TBCropDTO> cropsByNetHouse = cropDTOS.stream()
            .filter(cropDTO -> tbEntityDTO.getId().equals(cropDTO.getNetHouseId()))
            .collect(Collectors.toList());

        for (TBCropDTO cropDTO : cropsByNetHouse) {
            detailUserDTOS.addAll(cropDTO.getTbDetailUsers().stream().collect(Collectors.toList()));
        }

        return detailUserDTOS.stream()
            .filter(distinctByKey(TBDetailUserDTO::getId))
            .collect(Collectors.toList());
    }

    public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Set<Object> seen = ConcurrentHashMap.newKeySet();
        return t -> seen.add(keyExtractor.apply(t));
    }

    public Integer getEntityColor(TBEntityDTO tbEntityDTO, List<Object[]> activitiesOfNetHouses) {
        // There is no activity
        if (activitiesOfNetHouses.isEmpty()) {
            return null;
        }

        List<Object[]> activitiesByNetHouse = activitiesOfNetHouses.stream()
            .filter(netHouse -> tbEntityDTO.getId().equals(((BigInteger) netHouse[0]).longValue()))
            .collect(Collectors.toList());

        if (activitiesByNetHouse.isEmpty()) {
            return null;
        }

        List<Object[]> activeCropActivities = activitiesByNetHouse.stream()
            .filter(netHouse -> STATUS_ARE_ACTIVE.name().equals(netHouse[2]))
            .collect(Collectors.toList());

        List<Object[]> finishedCropActivities = activitiesByNetHouse.stream()
            .filter(netHouse -> STATUS_FINISHED.name().equals(netHouse[2]))
            .collect(Collectors.toList());

        // Finished net house
        if (activitiesByNetHouse.size() == finishedCropActivities.size()) {
            return PURPLE.getValue();
        }

        List<Object[]> pestsActivities = activeCropActivities.stream()
            .filter(netHouse -> Constants.ACTIVE_TYPE_PESTS_INVESTIGATION.equals(netHouse[4]))
            .collect(Collectors.toList());

        if (!pestsActivities.isEmpty()) {
            Object[] lastPestsActivity = Collections.max(pestsActivities, Comparator.comparing(c -> (Timestamp) c[3]));
            // Pests
            Object objPercentOfPlantEffect = lastPestsActivity[5];
            if (objPercentOfPlantEffect != null &&
                tryParseInt((String) objPercentOfPlantEffect) &&
                Integer.parseInt((String) objPercentOfPlantEffect) > 30) {
                return RED.getValue();
            }
        }

        // Harvesting
        List<Object[]> harvestActivities = activeCropActivities.stream()
            .filter(netHouse -> Constants.ACTIVE_TYPE_HARVEST.equals(netHouse[4]))
            .collect(Collectors.toList());
        if (!harvestActivities.isEmpty()) {
            return YELLOW.getValue();
        }

        List<Object[]> nurseryActivities = activeCropActivities.stream()
            .filter(netHouse -> Constants.ACTIVE_TYPE_NURSERY.equals(netHouse[4]))
            .collect(Collectors.toList());
        if (activitiesByNetHouse.size() == nurseryActivities.size()) {
            return GREEN.getValue();
        } else {
            return BLUE.getValue();
        }
    }

    boolean tryParseInt(String value) {
        try {
            Integer.parseInt(value);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    public List<Object[]> getAllActivitiesOfNetHouses() {
        String sqlString =
            " select \n" +
                "entity.id as netHouseId,\n" +
                "crop.id as cropId,\n" +
                "crop.status as cropStatus,\n" +
                "activity.execute_date as executeDate,\n" +
                "activityType.name as activityTypeName,\n" +
                "pestsInvestigation.updateIndex as percentOfPlantEffect\n" +
                "FROM tb_entity as entity\n" +
                "inner join tb_crop as crop on entity.id = crop.tb_entity_id\n" +
                "inner join tb_activity as activity on activity.tb_crop_id = crop.id\n" +
                "inner join tb_activity_type as activityType on activity.activity_type = activityType.id\n" +
                "left join (\n" +
                "  select  objUpdate.tb_activity_id as activityId, objUpdate.jhi_index as updateIndex\n" +
                "  from tb_object_update as objUpdate\n" +
                "  inner join tb_object_parameter as objParam on objUpdate.tb_object_parameter_id = objParam.id\n" +
                "  where objParam.name = :percentPlantParam) pestsInvestigation on pestsInvestigation.activityId = activity.id\n" +
                "order by activity.execute_date asc";
        Query query = em.createNativeQuery(sqlString);
        query.setParameter("percentPlantParam", Constants.PHAN_TRAM_CAY);

        return (List<Object[]>) query.getResultList();
    }

    public List<Object>getUnits(String unitType){
        List<Object> result = new ArrayList<>();
        switch (unitType) {
            case "weight":
                for (Constants.UNIT_WEIGHT item : Constants.UNIT_WEIGHT.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                break;
            case "size":
                for (Constants.UNIT_SIZE item : Constants.UNIT_SIZE.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                break;
            case "capacity":
                for (Constants.UNIT_CAPACITY item : Constants.UNIT_CAPACITY.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                break;
            case "seed":
                for (Constants.UNIT_SEED item : Constants.UNIT_SEED.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                break;
            default:
                for (Constants.UNIT_WEIGHT item : Constants.UNIT_WEIGHT.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                for (Constants.UNIT_SIZE item : Constants.UNIT_SIZE.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                for (Constants.UNIT_CAPACITY item : Constants.UNIT_CAPACITY.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                for (Constants.UNIT_SEED item : Constants.UNIT_SEED.values()) {
                    HashMap<String, String> data = new HashMap<String, String>();
                    data.put("unit", item.name());
                    data.put("name", item.getValue());
                    result.add(data);
                }
                break;
        }
        return result;
    }

}
