package vn.azteam.tpf.service;

import java.math.BigInteger;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.*;

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

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.Propagation;
import org.springframework.transaction.annotation.Transactional;

import io.github.jhipster.service.QueryService;

import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import vn.azteam.tpf.domain.TBActivity;
import vn.azteam.tpf.domain.*; // for static metamodels
import vn.azteam.tpf.repository.TBActivityRepository;
import vn.azteam.tpf.service.dto.*;
import vn.azteam.tpf.service.mapper.TBActivityMapper;
import static vn.azteam.tpf.Constants.Constants.*;

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

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

    private final TBActivityRepository tBActivityRepository;

    private final TBActivityMapper tBActivityMapper;

    private final FileStorageService fileStorageService;

    private final TBActivityService tBActivityService;

    private final UserService userService;

    private final TBCropQueryService tBCropQueryService;

    private final TBActivityTypeQueryService tBActivityTypeQueryService;

    private final TBCropService tBCropService;

    private final EntityManager em;

    private static final String ACTIVE_TYPE_OTHER = "ACTIVE_TYPE_OTHER";

    public TBActivityQueryService(TBActivityRepository tBActivityRepository,
                                  TBActivityMapper tBActivityMapper,
                                  FileStorageService fileStorageService,
                                  TBActivityService tBActivityService,
                                  UserService userService,
                                  TBCropQueryService tBCropQueryService,
                                  TBActivityTypeQueryService tBActivityTypeQueryService,
                                  TBCropService tBCropService,
                                  EntityManager em) {
        this.tBActivityRepository = tBActivityRepository;
        this.tBActivityMapper = tBActivityMapper;
        this.fileStorageService = fileStorageService;
        this.tBActivityService = tBActivityService;
        this.userService = userService;
        this.tBCropQueryService = tBCropQueryService;
        this.tBActivityTypeQueryService = tBActivityTypeQueryService;
        this.tBCropService = tBCropService;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link TBActivityDTO} 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<TBActivityDTO> findByCriteria(TBActivityCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<TBActivity> specification = createSpecification(criteria);
        return tBActivityMapper.toDto(tBActivityRepository.findAll(specification));
    }

    /**
     * Return a {@link Page} of {@link TBActivityDTO} 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<TBActivityDTO> findByCriteria(TBActivityCriteria criteria, Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        final Specification<TBActivity> specification = createSpecification(criteria);
        return tBActivityRepository.findAll(specification, page)
            .map(tBActivityMapper::toDto);
    }

    @Transactional(readOnly = true)
    public Page<TBActivityDTO> findByCriteriaExceptAutomatically(TBActivityCriteria criteria, Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        final Specification<TBActivity> specification = createSpecExceptUpdateEnvAutomatically(criteria);
        return tBActivityRepository.findAll(specification, page)
            .map(tBActivityMapper::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(TBActivityCriteria criteria) {
        log.debug("count by criteria : {}", criteria);
        final Specification<TBActivity> specification = createSpecification(criteria);
        return tBActivityRepository.count(specification);
    }

    /**
     * Function to convert TBActivityCriteria to a {@link Specification}
     */
    private Specification<TBActivity> createSpecification(TBActivityCriteria criteria) {
        Specification<TBActivity> specification = Specification.where(null);
        if (criteria != null) {
            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), TBActivity_.id));
            }
            if (criteria.getAgeDay() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getAgeDay(), TBActivity_.ageDay));
            }
            if (criteria.getTotalCost() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getTotalCost(), TBActivity_.totalCost));
            }
            if (criteria.getMedia() != null) {
                specification = specification.and(buildStringSpecification(criteria.getMedia(), TBActivity_.media));
            }
            if (criteria.getExternalTable() != null) {
                specification = specification.and(buildStringSpecification(criteria.getExternalTable(), TBActivity_.externalTable));
            }
            if (criteria.getExecuteDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getExecuteDate(), TBActivity_.executeDate));
            }
            if (criteria.getDescription() != null) {
                specification = specification.and(buildStringSpecification(criteria.getDescription(), TBActivity_.description));
            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), TBActivity_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), TBActivity_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), TBActivity_.deletedDate));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(TBActivity_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(TBActivity_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(TBActivity_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getActivityTypeId() != null) {
                specification = specification.and(buildSpecification(criteria.getActivityTypeId(),
                    root -> root.join(TBActivity_.activityType, JoinType.LEFT).get(TBActivityType_.id)));
            }
            if (criteria.getCropId() != null) {
                specification = specification.and(buildSpecification(criteria.getCropId(),
                    root -> root.join(TBActivity_.crop, JoinType.LEFT).get(TBCrop_.id)));
            }
        }
        return specification;
    }

    private Specification<TBActivity> createSpecExceptUpdateEnvAutomatically(TBActivityCriteria criteria) {
        Specification<TBActivity> specification = createSpecification(criteria);
        return Specification
            .where(exceptUpdateEnvAutomaticallySpec())
            .and(specification);
    }

    private Specification<TBActivity> exceptUpdateEnvAutomaticallySpec() {
        return (Specification<TBActivity>) (root, query, cb) -> {
            final Join<TBActivity, TBDetailUser> detailUserJoin = root.join(TBActivity_.createdBy, JoinType.LEFT);
            final Join<TBActivity, TBActivityType> activityTypeJoin = root.join(TBActivity_.activityType, JoinType.LEFT);
            Predicate updateEnvBuilder = cb.and(cb.equal(activityTypeJoin.get(TBActivityType_.name), ACTIVE_TYPE_UPDATE_ENV));
            Predicate createdByNotNullBuilder = cb.and(cb.isNotNull(detailUserJoin.get(TBDetailUser_.id)));
            Predicate updateEnvExceptAutomaticallyBuilder = cb.and(updateEnvBuilder, createdByNotNullBuilder);
            Predicate activityOtherEnvUpdateBuilder = cb.and(
                cb.notEqual(activityTypeJoin.get(TBActivityType_.name),ACTIVE_TYPE_UPDATE_ENV));
            return cb.or(updateEnvExceptAutomaticallyBuilder, activityOtherEnvUpdateBuilder);
        };
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public TBActivityDTO saveUploadedFiles(TBActivityDTO tBActivityDTO, MultipartFile[] images, String path) {
        String imagesPath;
        if (null != images && images.length > 0) {
            if (images.length == 1 && Objects.equals(images[0].getOriginalFilename(), "")) {
                tBActivityDTO.setMedia(tBActivityDTO.getMedia());
            } else {
                imagesPath = this.fileStorageService.storeFiles(path, images);
                if (tBActivityDTO.getMedia() != null && !tBActivityDTO.getMedia().equals("")) {
                    tBActivityDTO.setMedia(tBActivityDTO.getMedia() + ";" + imagesPath);
                } else {
                    tBActivityDTO.setMedia(imagesPath);
                }
            }
        } else {
            tBActivityDTO.setMedia(tBActivityDTO.getMedia());
        }
        return tBActivityDTO;
    }

    public TBActivityDTO updateUploadedFiles(TBActivityDTO tBActivityDTO,
                                             MultipartFile[] images,
                                             String path,
                                             List<String> deleteImages) {
        if (null != deleteImages) {
            ArrayList<String> lstOriginalImages = convertStringToList(tBActivityDTO.getMedia());
            for (String deleteImage : deleteImages) {
                this.fileStorageService.removeFile("", deleteImage);
                lstOriginalImages.remove(deleteImage);
            }
            tBActivityDTO.setMedia(convertListToString(lstOriginalImages));
        }
        tBActivityDTO = this.saveUploadedFiles(tBActivityDTO, images, path);
        return tBActivityDTO;
    }


    private ArrayList<String> convertStringToList(String imagesString) {
        ArrayList<String> lstImages = new ArrayList<>();
        if (!StringUtils.isEmpty(imagesString)) {
            lstImages.addAll(Arrays.asList(imagesString.split(";")));
        }
        return lstImages;
    }

    private String convertListToString(ArrayList<String> lstOriginalImages) {
        final String[] url = {""};
        lstOriginalImages.forEach(item -> {
            if (url[0].equals("")) {
                url[0] = item;
            } else {
                url[0] = url[0] + ";" + item;
            }
        });

        return url[0];
    }
}
