package vn.azteam.tpf.service;

import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.criteria.*;

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.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.domain.TBGuideline;
import vn.azteam.tpf.domain.*; // for static metamodels
import vn.azteam.tpf.repository.TBGuidelineRepository;
import vn.azteam.tpf.repository.TBSuppliesRepository;
import vn.azteam.tpf.repository.UserRepository;
import vn.azteam.tpf.repository.search.TBGuidelineSearchRepository;
import vn.azteam.tpf.security.SecurityUtils;
import vn.azteam.tpf.service.dto.*;
import vn.azteam.tpf.service.mapper.TBGuidelineMapper;

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

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

    private final TBGuidelineRepository tBGuidelineRepository;

    private final TBGuidelineMapper tBGuidelineMapper;

    private final TBGuidelineSearchRepository tBGuidelineSearchRepository;

    private final TBGuidelineDetailsQueryService tBGuidelineDetailsQueryService;

    private final TBGuidelinePeriodQueryService tBGuidelinePeriodQueryService;

    private final TBSuppliesRepository tBSuppliesRepository;

    private final UserRepository userRepository;

    private final UserService userService;

    private final EntityManager em;

    public TBGuidelineQueryService(
        TBGuidelineRepository tBGuidelineRepository,
        TBGuidelineMapper tBGuidelineMapper,
        TBGuidelineSearchRepository tBGuidelineSearchRepository,
        TBGuidelineDetailsQueryService tBGuidelineDetailsQueryService,
        TBGuidelinePeriodQueryService tBGuidelinePeriodQueryService,
        TBSuppliesRepository tBSuppliesRepository,
        UserService userService,
        UserRepository userRepository,
        EntityManager em) {
        this.tBGuidelineRepository = tBGuidelineRepository;
        this.tBGuidelineMapper = tBGuidelineMapper;
        this.tBGuidelineSearchRepository = tBGuidelineSearchRepository;
        this.tBGuidelineDetailsQueryService = tBGuidelineDetailsQueryService;
        this.tBGuidelinePeriodQueryService = tBGuidelinePeriodQueryService;
        this.tBSuppliesRepository = tBSuppliesRepository;
        this.userRepository = userRepository;
        this.userService = userService;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link TBGuidelineDTO} 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<TBGuidelineDTO> findByCriteria(TBGuidelineCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<TBGuideline> specification = createSpecification(criteria);
        return tBGuidelineMapper.toDto(tBGuidelineRepository.findAll(specification));
    }

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

    /**
     * Function to convert TBGuidelineCriteria to a {@link Specification}
     */
    private Specification<TBGuideline> createSpecification(TBGuidelineCriteria criteria) {
        Specification<TBGuideline> specification = initializeGuidelineSpecification();
        if (criteria != null) {
            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), TBGuideline_.id));
            }
            if (criteria.getCode() != null) {
                specification = specification.and(buildStringSpecification(criteria.getCode(), TBGuideline_.code));
            }
            if (criteria.getName() != null) {
                specification = specification.and(buildStringSpecification(criteria.getName(), TBGuideline_.name));
            }
            if (criteria.getDuration() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDuration(), TBGuideline_.duration));
            }
            if (criteria.getDescription() != null) {
                specification = specification.and(buildStringSpecification(criteria.getDescription(), TBGuideline_.description));
            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TBGuideline_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), TBGuideline_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), TBGuideline_.deletedDate));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(TBGuideline_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(TBGuideline_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(TBGuideline_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getTbCustomerId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbCustomerId(),
                    root -> root.join(TBGuideline_.tbCustomer, JoinType.LEFT).get(TBCustomer_.id)));
            }
            if (criteria.getTbSuppliesId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbSuppliesId(),
                    root -> root.join(TBGuideline_.tbSupplies, JoinType.LEFT).get(TBSupplies_.id)));
            }
        }
        return specification;
    }

    private Specification<TBGuideline> initializeGuidelineSpecification() {
        return (Specification<TBGuideline>) (root, query, cb) -> {
            final Join<TBGuideline, TBDetailUser> detailUserJoin = root.join(TBGuideline_.deletedBy, JoinType.LEFT);
            return cb.isNull(detailUserJoin.get(TBDetailUser_.id));
        };
    }

    private Specification<TBGuideline> createSearchGuidelineSpecification(TBGuidelineCriteria criteria) {
        Specification<TBGuideline> specification = Specification.where(null);
        if (criteria != null) {
            if (criteria.getCode() != null) {
                specification = specification.or(buildStringSpecification(criteria.getCode(), TBGuideline_.code));
            }
            if (criteria.getName() != null) {
                specification = specification.or(buildStringSpecification(criteria.getName(), TBGuideline_.name));
            }
            if (criteria.getDescription() != null) {
                specification = specification.or(buildStringSpecification(criteria.getDescription(), TBGuideline_.description));
            }
        }
        return Specification.where(initializeGuidelineSpecification()).and(specification);
    }

    public Optional<GuidelineWrapperDTO> getFullInfoGuideline(Long id) {

        GuidelineWrapperDTO guidelineWrapperDTO = new GuidelineWrapperDTO();

        Optional<TBGuidelineDTO> guidelineDTO = tBGuidelineRepository.findById(id)
            .map(tBGuidelineMapper::toDto);
        UserDTO currentUser = userService.getCurrentUserDTO().get();
        if (currentUser.getCustomerId() != null
            && guidelineDTO.get().getTbCustomerId() != null
            && currentUser.getCustomerId() != guidelineDTO.get().getTbCustomerId()){
            return Optional.empty();
        }
        if (!guidelineDTO.isPresent()) {
            return Optional.empty();
        }

        TBGuidelineDetailsCriteria criteria = new TBGuidelineDetailsCriteria();
        LongFilter guidelineFilter = new LongFilter();
        guidelineFilter.setEquals(id);
        criteria.setTbGuidelineId(guidelineFilter);
        List<TBGuidelineDetailsDTO> guidelineDetailsDTOS = tBGuidelineDetailsQueryService.findByCriteria(criteria);
        guidelineDetailsDTOS.sort(Comparator.comparing(TBGuidelineDetailsDTO::getAgeDayStart));

        TBGuidelinePeriodCriteria criteria2 = new TBGuidelinePeriodCriteria();
        criteria2.setTbGuidelineId(guidelineFilter);
        List<TBGuidelinePeriodDTO> guidelinePeriodDTOs = tBGuidelinePeriodQueryService.findByCriteria(criteria2);

        guidelineWrapperDTO.setGuideline(guidelineDTO.get());
        guidelineWrapperDTO.setGuidelineDetails(guidelineDetailsDTOS);
        guidelineWrapperDTO.setGuidelinePeriods(guidelinePeriodDTOs);

        return Optional.of(guidelineWrapperDTO);
    }

    @Transactional()
    public TBGuidelineDTO updateFullInfoGuideline(GuidelineWrapperDTO tBGuidelineWrapperDTO) {

        TBGuidelineDTO guidelineDTOInput = tBGuidelineWrapperDTO.getGuideline();

        TBGuideline tBGuideline = tBGuidelineMapper.toEntity(guidelineDTOInput);
        tBGuideline.setModifiedDate(Instant.now());
        tBGuideline.setModifiedBy(SecurityUtils.getCurrentUserLogin()
            .flatMap(userRepository::findOneByLogin).get().getTBDetailUser());
        tBGuideline = tBGuidelineRepository.save(tBGuideline);
        TBGuidelineDTO result = tBGuidelineMapper.toDto(tBGuideline);
        tBGuidelineSearchRepository.save(tBGuideline);

        deleteSuppliesHasGuideline(result.getId());

        List<Long> supplyIds = guidelineDTOInput
            .getTbSupplies()
            .stream()
            .map(TBSuppliesDTO::getId)
            .collect(Collectors.toList());
        if (!supplyIds.isEmpty()) {
            insertSuppliesHasGuideline(result.getId(), supplyIds);
        }

        return result;
    }

    public Page<TBGuidelineDTO> searchTBGuidelines(String query, Pageable page) {
        TBGuidelineCriteria criteria = new TBGuidelineCriteria();
        if (query != null && !query.isEmpty()) {
            StringFilter queryFilter = new StringFilter();
            queryFilter.setContains(query);
            criteria.setCode(queryFilter);
            criteria.setName(queryFilter);
            criteria.setDescription(queryFilter);
        }

        log.debug("search guideline by criteria : {}, page: {}", criteria, page);
        final Specification<TBGuideline> specification = createSearchGuidelineSpecification(criteria);
        return tBGuidelineRepository.findAll(specification, page)
            .map(tBGuidelineMapper::toDto);
    }

    private int deleteSuppliesHasGuideline(Long guidelineId) {
        String sqlString = String.format(
            "delete from tb_supplies_has_guideline where tb_guidelines_id = :guidelineId");

        return em.createNativeQuery(sqlString)
            .setParameter("guidelineId", guidelineId)
            .executeUpdate();
    }

    private int insertSuppliesHasGuideline(Long guidelineId, List<Long> supplyIds) {
        String insertedValues = "";

        for (Long supplyId : supplyIds) {
            if (!tBSuppliesRepository.existsById(supplyId)) {
                continue;
            }
            insertedValues += String.format("(%s, %s),", guidelineId, supplyId);
        }
        if (insertedValues.isEmpty()) {
            return 0;
        }
        insertedValues = insertedValues.substring(0, insertedValues.length() - 1);
        String sqlString = String.format(
            "insert into tb_supplies_has_guideline (tb_guidelines_id, tb_supplies_id)\n" +
                " values%s", insertedValues);

        return em.createNativeQuery(sqlString).executeUpdate();
    }

    private int deleteGuidelineDetails(List<Long> guidelineDetailIds) {
        String sqlString = String.format(
            "delete from tb_guideline_details where id in (:guidelineDetailIds)");

        return em.createNativeQuery(sqlString)
            .setParameter("guidelineDetailIds", guidelineDetailIds)
            .executeUpdate();
    }
}
