package vn.azteam.tpf.service;

import java.time.Instant;
import java.util.List;
import java.util.Optional;

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

import com.google.common.collect.Lists;
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 io.github.jhipster.service.filter.LongFilter;
import vn.azteam.tpf.domain.TBCustomer;
import vn.azteam.tpf.domain.*; // for static metamodels
import vn.azteam.tpf.repository.TBCustomerRepository;
import vn.azteam.tpf.repository.search.TBCustomerSearchRepository;
import vn.azteam.tpf.service.dto.TBAddressDTO;
import vn.azteam.tpf.service.dto.TBCustomerCriteria;
import vn.azteam.tpf.service.dto.TBCustomerDTO;
import vn.azteam.tpf.service.dto.UserDTO;
import vn.azteam.tpf.service.mapper.TBCustomerMapper;

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

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

    private final TBCustomerRepository tBCustomerRepository;

    private final TBCustomerMapper tBCustomerMapper;

    private final TBCustomerSearchRepository tBCustomerSearchRepository;

    private final TBCustomerService tBCustomerService;

    private final TBAddressService tBAddressService;

    private final UserService userService;

    private final EntityManager em;

    public TBCustomerQueryService(TBCustomerRepository tBCustomerRepository, TBCustomerMapper tBCustomerMapper, TBCustomerSearchRepository tBCustomerSearchRepository, TBCustomerService tBCustomerService, TBAddressService tBAddressService, UserService userService, EntityManager em) {
        this.tBCustomerRepository = tBCustomerRepository;
        this.tBCustomerMapper = tBCustomerMapper;
        this.tBCustomerSearchRepository = tBCustomerSearchRepository;
        this.tBCustomerService = tBCustomerService;
        this.tBAddressService = tBAddressService;
        this.userService = userService;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link TBCustomerDTO} 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<TBCustomerDTO> findByCriteria(TBCustomerCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<TBCustomer> specification = createSpecification(criteria);
        return tBCustomerMapper.toDto(tBCustomerRepository.findAll(specification));
    }

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

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

            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), TBCustomer_.id));
            }
            if (criteria.getName() != null) {
                specification = specification.and(buildStringSpecification(criteria.getName(), TBCustomer_.name));
            }
            if (criteria.getPhone() != null) {
                specification = specification.and(buildStringSpecification(criteria.getPhone(), TBCustomer_.phone));
            }
            if (criteria.getEmail() != null) {
                specification = specification.and(buildStringSpecification(criteria.getEmail(), TBCustomer_.email));
            }
            if (criteria.getSlugName() != null) {
                specification = specification.and(buildStringSpecification(criteria.getSlugName(), TBCustomer_.slugName));
            }
            if (criteria.getAreaM2() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getAreaM2(), TBCustomer_.areaM2));
            }
            if (criteria.getIsActivated() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getIsActivated(), TBCustomer_.isActivated));
            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TBCustomer_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), TBCustomer_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), TBCustomer_.deletedDate));
            }
            if (criteria.getTbAddressId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbAddressId(),
                    root -> root.join(TBCustomer_.tbAddress, JoinType.LEFT).get(TBAddress_.id)));
            }
            if (criteria.getTbDashboardTypeId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbDashboardTypeId(),
                    root -> root.join(TBCustomer_.tbDashboardType, JoinType.LEFT).get(TBDashboardType_.id)));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(TBCustomer_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(TBCustomer_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(TBCustomer_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }

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

    @Transactional()
    public TBCustomerDTO createCustomer(TBCustomerDTO tBCustomerDTO){
        Optional<UserDTO> currentUser = userService.getCurrentUserDTO();

        TBAddressDTO tbAddressDTO = new TBAddressDTO();
        tbAddressDTO.setId(tBCustomerDTO.getTbAddressId());
        tbAddressDTO.setTbCountryId(tBCustomerDTO.getCountryId());
        tbAddressDTO.setTbCityId(tBCustomerDTO.getCityId());
        tbAddressDTO.setTbDistrictId(tBCustomerDTO.getDistrictId());
        tbAddressDTO.setTbWardId(tBCustomerDTO.getWardId());
        tbAddressDTO.setAddress(tBCustomerDTO.getAddress());
        TBAddressDTO savedTBAddressDTO = this.tBAddressService.save(tbAddressDTO);

        tBCustomerDTO.setCreatedById(currentUser.get().getId());
        tBCustomerDTO.setCreatedDate(Instant.now());
        tBCustomerDTO.setTbAddressId(savedTBAddressDTO.getId());
        return tBCustomerService.save(tBCustomerDTO);
    }

    @Transactional()
    public TBCustomerDTO updateCustomer(TBCustomerDTO tBCustomerDTO){
        Optional<UserDTO> currentUser = userService.getCurrentUserDTO();

        TBAddressDTO tbAddressDTO = new TBAddressDTO();
        Optional<TBCustomerDTO> dbTBCustomer = tBCustomerService.findOne(tBCustomerDTO.getId());
        if(dbTBCustomer.isPresent() && dbTBCustomer.get().getTbAddressId() != null)
        {
            Optional<TBAddressDTO> optionalTBAddressDTO = tBAddressService.findOne(dbTBCustomer.get().getTbAddressId());
            if (optionalTBAddressDTO.isPresent()) {
                tbAddressDTO = optionalTBAddressDTO.get();
            }
        }
        tbAddressDTO.setTbCountryId(tBCustomerDTO.getCountryId());
        tbAddressDTO.setTbCityId(tBCustomerDTO.getCityId());
        tbAddressDTO.setTbDistrictId(tBCustomerDTO.getDistrictId());
        tbAddressDTO.setTbWardId(tBCustomerDTO.getWardId());
        tbAddressDTO.setAddress(tBCustomerDTO.getAddress());
        TBAddressDTO savedTBAddressDTO = this.tBAddressService.save(tbAddressDTO);

        tBCustomerDTO.setModifiedById(currentUser.get().getId());
        tBCustomerDTO.setModifiedDate(Instant.now());
        tBCustomerDTO.setTbAddressId(savedTBAddressDTO.getId());
        return tBCustomerService.save(tBCustomerDTO);
    }

    public Page<TBCustomerDTO> searchCustomer(String query, String status, Pageable pageable) {
        String sqlString = "select c.*\n" +
            " from tb_customer as c\n" +
            " join tb_address as a on c.tb_address_id = a.id\n" +
            " where 1 = 1";

        if(query != null && !query.isEmpty()) {
            sqlString = sqlString + " and ( c.name like '%" + query + "%'\n" +
                        " or c.phone like '%" + query + "%'\n" +
                        " or c.email like '%" + query + "%'\n" +
                        " or a.address like '%" + query + "%')\n";
        }

        if(status != null && !status.isEmpty()) {
            int statusNumber = Integer.parseInt(status);
            sqlString = sqlString + " and (c.status = " + statusNumber + ")";
        }

        Query tbCustomerQuery = em.createNativeQuery(sqlString, TBCustomer.class);
        List<TBCustomerDTO> tbCustomerDTOs = tBCustomerMapper.toDto(tbCustomerQuery.getResultList());
        int start = Math.toIntExact(pageable.getOffset());
        int end = Math.toIntExact((start + pageable.getPageSize()) > tbCustomerDTOs.size() ? tbCustomerDTOs.size() : (start + pageable.getPageSize()));
        if(tbCustomerDTOs.size() > start) {
            return new PageImpl<>(tbCustomerDTOs.subList(start, end), pageable, tbCustomerDTOs.size());
        }
        return new PageImpl<>(Lists.newArrayList(), pageable, tbCustomerDTOs.size());
    }
}
