package vn.azteam.tpf.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

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

import com.google.api.client.util.Lists;
import io.github.jhipster.service.filter.LongFilter;
import io.github.jhipster.service.filter.StringFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
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 io.github.jhipster.service.QueryService;

import vn.azteam.tpf.Constants.Constants;
import vn.azteam.tpf.domain.TBSupplies;
import vn.azteam.tpf.domain.*; // for static metamodels
import vn.azteam.tpf.repository.TBSuppliesRepository;
import vn.azteam.tpf.repository.search.TBSuppliesSearchRepository;
import vn.azteam.tpf.service.dto.*;
import vn.azteam.tpf.service.mapper.TBSuppliesMapper;
import static vn.azteam.tpf.web.rest.util.ConvertCharacterUtil.removeAccent;
import static vn.azteam.tpf.Constants.Constants.ENUM_SUPPLIES_TYPE.GIONG;

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

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

    private final TBSuppliesRepository tBSuppliesRepository;

    private final TBSuppliesMapper tBSuppliesMapper;
    private final TBSuppliesService tBSuppliesService;

    private final TBSuppliesSearchRepository tBSuppliesSearchRepository;

    private final UserService userService;

    private final TBSuppliesInWarehouseQueryService tBSuppliesInWarehouseQueryService;

    private final TBStockUpdateQueryService tBStockUpdateQueryService;

    private final EntityManager em;

    public TBSuppliesQueryService(
        TBSuppliesRepository tBSuppliesRepository,
        TBSuppliesMapper tBSuppliesMapper,
        TBSuppliesService tBSuppliesService,
        TBSuppliesSearchRepository tBSuppliesSearchRepository,
        UserService userService,
        TBSuppliesInWarehouseQueryService tBSuppliesInWarehouseQueryService,
        TBStockUpdateQueryService tBStockUpdateQueryService,
        EntityManager em) {
        this.tBSuppliesRepository = tBSuppliesRepository;
        this.tBSuppliesMapper = tBSuppliesMapper;
        this.tBSuppliesService = tBSuppliesService;
        this.tBSuppliesSearchRepository = tBSuppliesSearchRepository;
        this.userService = userService;
        this.tBSuppliesInWarehouseQueryService = tBSuppliesInWarehouseQueryService;
        this.tBStockUpdateQueryService = tBStockUpdateQueryService;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link TBSuppliesDTO} 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<TBSuppliesDTO> findByCriteria(TBSuppliesCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<TBSupplies> specification = createSpecification(criteria);
        return tBSuppliesMapper.toDto(tBSuppliesRepository.findAll(specification));
    }

    /**
     * Return a {@link Page} of {@link TBSuppliesDTO} 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<TBSuppliesDTO> findByCriteria(TBSuppliesCriteria criteria, Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        final Specification<TBSupplies> specification = createSpecification(criteria);
        return tBSuppliesRepository.findAll(specification, page)
            .map(tBSuppliesMapper::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(TBSuppliesCriteria criteria) {
        log.debug("count by criteria : {}", criteria);
        final Specification<TBSupplies> specification = createSpecification(criteria);
        return tBSuppliesRepository.count(specification);
    }

    /**
     * Function to convert TBSuppliesCriteria to a {@link Specification}
     */
    private Specification<TBSupplies> createSpecification(TBSuppliesCriteria criteria) {
        Specification<TBSupplies> specification = Specification.where(null);
        if (criteria != null) {
            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), TBSupplies_.id));
            }
            if (criteria.getName() != null) {
                specification = specification.and(buildStringSpecification(criteria.getName(), TBSupplies_.name));
            }
            if (criteria.getIngredient() != null) {
                specification = specification.and(buildStringSpecification(criteria.getIngredient(), TBSupplies_.ingredient));
            }
            if (criteria.getUse() != null) {
                specification = specification.and(buildStringSpecification(criteria.getUse(), TBSupplies_.use));
            }
            if (criteria.getSku() != null) {
                specification = specification.and(buildStringSpecification(criteria.getSku(), TBSupplies_.sku));
            }
            if (criteria.getDescription() != null) {
                specification = specification.and(buildStringSpecification(criteria.getDescription(), TBSupplies_.description));
            }
            if (criteria.getManufacturer() != null) {
                specification = specification.and(buildStringSpecification(criteria.getManufacturer(), TBSupplies_.manufacturer));
            }
            if (criteria.getQuarantinePeriod() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getQuarantinePeriod(), TBSupplies_.quarantinePeriod));
            }
            if (criteria.getDosage() != null) {
                specification = specification.and(buildStringSpecification(criteria.getDosage(), TBSupplies_.dosage));
            }
            if (criteria.getPackaging() != null) {
                specification = specification.and(buildStringSpecification(criteria.getPackaging(), TBSupplies_.packaging));
            }
            if (criteria.getJhiSize() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getJhiSize(), TBSupplies_.jhiSize));
            }
            if (criteria.getUnit() != null) {
                specification = specification.and(buildStringSpecification(criteria.getUnit(), TBSupplies_.unit));
            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TBSupplies_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), TBSupplies_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), TBSupplies_.deletedDate));
            }
            if (criteria.getTbSuppliesTypeId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbSuppliesTypeId(),
                    root -> root.join(TBSupplies_.tbSuppliesType, JoinType.LEFT).get(TBSuppliesType_.id)));
            }
            if (criteria.getTbSuppliesTypeName() != null) {
                specification = specification.and(buildSpecification(criteria.getTbSuppliesTypeName(),
                    root -> root.join(TBSupplies_.tbSuppliesType, JoinType.LEFT).get(TBSuppliesType_.name)));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(TBSupplies_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(TBSupplies_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(TBSupplies_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getTbCustomerId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbCustomerId(),
                    root -> root.join(TBSupplies_.tbCustomer, JoinType.LEFT).get(TBCustomer_.id)));
            }
            if (criteria.getTbGuidelineId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbGuidelineId(),
                    root -> root.join(TBSupplies_.tbGuidelines, JoinType.LEFT).get(TBGuideline_.id)));
            }
        }
        return specification;
    }

    private Specification<TBSupplies> createSearchSeedSpecification(TBSuppliesCriteria criteria) {
        Specification<TBSupplies> specification = Specification.where(null);
        if (criteria != null) {
            if (criteria.getName() != null) {
                specification = specification.or(buildStringSpecification(criteria.getName(), TBSupplies_.name));
            }
            if (criteria.getIngredient() != null) {
                specification = specification.or(buildStringSpecification(criteria.getIngredient(), TBSupplies_.ingredient));
            }
            if (criteria.getUse() != null) {
                specification = specification.or(buildStringSpecification(criteria.getUse(), TBSupplies_.use));
            }
            if (criteria.getSku() != null) {
                specification = specification.or(buildStringSpecification(criteria.getSku(), TBSupplies_.sku));
            }
            if (criteria.getDescription() != null) {
                specification = specification.or(buildStringSpecification(criteria.getDescription(), TBSupplies_.description));
            }
            if (criteria.getManufacturer() != null) {
                specification = specification.or(buildStringSpecification(criteria.getManufacturer(), TBSupplies_.manufacturer));
            }
            if (criteria.getDosage() != null) {
                specification = specification.or(buildStringSpecification(criteria.getDosage(), TBSupplies_.dosage));
            }
            if (criteria.getPackaging() != null) {
                specification = specification.or(buildStringSpecification(criteria.getPackaging(), TBSupplies_.packaging));
            }
            if (criteria.getUnit() != null) {
                specification = specification.or(buildStringSpecification(criteria.getUnit(), TBSupplies_.unit));
            }
            if (criteria.getTbSuppliesTypeName() != null) {
                specification = specification.and(buildSpecification(criteria.getTbSuppliesTypeName(),
                    root -> root.join(TBSupplies_.tbSuppliesType, JoinType.LEFT).get(TBSuppliesType_.name)));
            }
            if (criteria.getTbCustomerId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbCustomerId(),
                    root -> root.join(TBSupplies_.tbCustomer, JoinType.LEFT).get(TBCustomer_.id)));
            }
        }
        return specification;
    }

    private Boolean queryContain(String data, String query) {
        if (data == null) {
            return Boolean.FALSE;
        } else {
            return removeAccent(data.toLowerCase()).contains(removeAccent(query.toLowerCase()));
        }
    }

    public Page<TBSuppliesDTO> getListTBSuppliesToCustomer(TBSuppliesCriteria criteria, Pageable page, String query, Long suppliesTypeId) {
        UserDTO currentUser = userService.getCurrentUserDTO().get();

        List<TBSupplies> data = tBSuppliesRepository.findAll().stream()
            .filter(item -> (item.getDeletedDate() == null) && (item.getDeletedBy() == null))
            .collect(Collectors.toList());
        if (currentUser.getCustomerId() != null) {
            data = data.stream()
                .filter(item -> item.getTbCustomer() == null || item.getTbCustomer().getId() == currentUser.getCustomerId())
                .collect(Collectors.toList());
        }
        if (suppliesTypeId != null) {
            data = data.stream()
                .filter(item -> item.getTbSuppliesType().getId() == suppliesTypeId)
                .collect(Collectors.toList());
        } else {
            //Exclude GIONG
            data = data.stream()
                .filter(item -> item.getTbSuppliesType() != null && item.getTbSuppliesType().getName() != null
                    && item.getTbSuppliesType().getName().equals(GIONG.name()) == Boolean.FALSE)
                .collect(Collectors.toList());
        }
        if (query != null && query.equals("") == Boolean.FALSE) {
            data = data.stream()
                .filter(item ->
                    queryContain(item.getName(), query)
                        || queryContain(item.getSku(), query)
                        || queryContain(item.getManufacturer(), query)
                        || queryContain(item.getPackaging(), query)
                        || queryContain(item.getIngredient(), query)
                )
                .collect(Collectors.toList());
        }

        List<Long> listSuppliesId = data.stream().map(TBSupplies::getId).collect(Collectors.toList());
        if (listSuppliesId.size() > 0) {
            LongFilter suppliesIdFilter = new LongFilter();
            suppliesIdFilter.setIn(listSuppliesId);
            criteria.setId(suppliesIdFilter);

            final Specification<TBSupplies> specification = createSpecification(criteria);
            Page<TBSuppliesDTO> pages = tBSuppliesRepository.findAll(specification, page)
                .map(tBSuppliesMapper::toDto);
            pages.forEach(item -> item.setInventory(tBSuppliesInWarehouseQueryService.totalInventory(item.getId())));
            return pages;
        } else {
            return new PageImpl<>(Lists.newArrayList(), page, 0);
        }
    }

    public Page<UnitDTO> getUnits(Pageable pageable) {

        List<UnitDTO> unitDTOS = new ArrayList<>();
        for (Constants.SEED_UNIT enumUnit : Constants.SEED_UNIT.values()) {
            UnitDTO unitDTO = new UnitDTO();
            unitDTO.setUnit(enumUnit.name());
            unitDTO.setDescription(enumUnit.getValue());
            unitDTOS.add(unitDTO);
        }
        return new PageImpl<>(unitDTOS, pageable, unitDTOS.size());
    }

    /**
     * Get ttBSupplies by "sku" or "name"
     *
     * @param sku
     * @return
     */
    public Optional<TBSuppliesDTO> findFirstBySkuAndCustomerIdExceptId(String sku, Long Id, Long tbCustomerId) {
        String sqlString =
            " select supply.* \n" +
                " from tb_supplies as supply \n" +
                " where supply.id <> :supplyId and supply.sku is not null and supply.sku <> '' and \n" +
                " supply.deleted_by is null and \n" +
                " supply.tb_customer_id = :tbCustomerId and \n" +
                " supply.sku = :supplySku \n" +
                " limit 1";
        Query query = em.createNativeQuery(sqlString, TBSupplies.class);
        query.setParameter("supplyId", Id);
        query.setParameter("tbCustomerId", tbCustomerId);
        return getTbSuppliesDTO(sku, query);
    }

    /**
     * Get ttBSupplies by "sku" or "name"
     *
     * @param sku
     * @return
     */
    public Optional<TBSuppliesDTO> findFirstBySkuAndCustomerId(String sku, Long tbCustomerId) {
        String sqlString =
            " select supply.* \n" +
                " from tb_supplies as supply \n" +
                " where supply.sku is not null and supply.sku <> '' and \n" +
                " supply.deleted_by is null and \n" +
                " supply.tb_customer_id = :tbCustomerId and \n" +
                " supply.sku = :supplySku \n" +
                " limit 1";
        Query query = em.createNativeQuery(sqlString, TBSupplies.class);
        query.setParameter("tbCustomerId", tbCustomerId);
        return getTbSuppliesDTO(sku, query);
    }

    private Optional<TBSuppliesDTO> getTbSuppliesDTO(String sku, Query query) {
        query.setParameter("supplySku", sku);

        List resultList = query.getResultList();
        if (resultList.isEmpty()) {
            return Optional.empty();
        }
        return Optional.of(tBSuppliesMapper.toDto((TBSupplies) resultList.get(0)));
    }

    public boolean checkNameAlreadyExist(TBSuppliesDTO tBSuppliesDTO) {
        boolean isCheckName = false; // not existed yet
        isCheckName = tBSuppliesRepository.findAll().stream()
            .anyMatch(t -> (tBSuppliesDTO.getId() == null
                ||  (tBSuppliesDTO.getId() != null && tBSuppliesDTO.getId() != t.getId()))
                && t.getName().toLowerCase().equals(tBSuppliesDTO.getName().toLowerCase())
            );
        return isCheckName;
    }

    public boolean checkSkuAlreadyExist(TBSuppliesDTO tBSuppliesDTO) {
        if (tBSuppliesDTO.getSku() == null || tBSuppliesDTO.getSku().equals("")) {
            return false;
        } else {
            return tBSuppliesRepository.findAll().stream()
                .anyMatch(t -> (tBSuppliesDTO.getId() == null
                    ||  (tBSuppliesDTO.getId() != null && tBSuppliesDTO.getId() != t.getId()))
                    &&t.getSku() != null && t.getSku().equals(tBSuppliesDTO.getSku()));
        }
    }
}
