package vn.azteam.tpf.service;

import io.github.jhipster.service.QueryService;
import io.github.jhipster.service.filter.LongFilter;
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 vn.azteam.tpf.domain.*;
import vn.azteam.tpf.repository.CasbinRepository;
import vn.azteam.tpf.repository.search.CasbinSearchRepository;
import vn.azteam.tpf.service.dto.CasbinCriteria;
import vn.azteam.tpf.service.dto.CasbinDTO;
import vn.azteam.tpf.service.dto.UserDTO;
import vn.azteam.tpf.service.mapper.CasbinMapper;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.JoinType;
import java.util.List;

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

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

    private final CasbinRepository casbinRepository;

    private final CasbinMapper casbinMapper;

    private final UserService userService;

    private final CasbinSearchRepository casbinSearchRepository;
    private final EntityManager em;

    public CasbinQueryService(final CasbinRepository casbinRepository, final CasbinMapper casbinMapper, final CasbinSearchRepository casbinSearchRepository, final UserService userService, final EntityManager em) {
        this.casbinRepository = casbinRepository;
        this.casbinMapper = casbinMapper;
        this.userService = userService;
        this.casbinSearchRepository = casbinSearchRepository;
        this.em = em;
    }

    /**
     * Return a {@link List} of {@link CasbinDTO} 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<CasbinDTO> findByCriteria(final CasbinCriteria criteria) {
        log.debug("find by criteria : {}", criteria);
        final Specification<Casbin> specification = createSpecification(criteria);
        return casbinMapper.toDto(casbinRepository.findAll(specification));
    }

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

    /**
     * Function to convert PermissionCriteria to a {@link Specification}
     */
    private Specification<Casbin> createSpecification(final CasbinCriteria criteria) {
        Specification<Casbin> specification = Specification.where(null);
        if (criteria != null) {
            if (criteria.getId() != null) {
                specification = specification.and(buildSpecification(criteria.getId(), Casbin_.id));
            }
            if (criteria.getpType() != null) {
                specification = specification.and(buildSpecification(criteria.getpType(), Casbin_.pType));
            }
            if (criteria.getV0() != null) {
                specification = specification.and(buildSpecification(criteria.getV0(), Casbin_.v0));
            }
            if (criteria.getV1() != null) {
                specification = specification.and(buildSpecification(criteria.getV1(), Casbin_.v1));
            }
            if (criteria.getV2() != null) {
                specification = specification.and(buildSpecification(criteria.getV2(), Casbin_.v2));
            }
            if (criteria.getV3() != null) {
                specification = specification.and(buildSpecification(criteria.getV3(), Casbin_.v3));
            }
            if (criteria.getV4() != null) {
                specification = specification.and(buildSpecification(criteria.getV4(), Casbin_.v4));
            }
            if (criteria.getV5() != null) {
                specification = specification.and(buildSpecification(criteria.getV5(), Casbin_.v5));
            }
//            if (criteria.getHasPermission() != null) {
//                specification = specification.and(buildRangeSpecification(criteria.getHasPermission(), Permission_.hasPermission));
//            }
            if (criteria.getCreatedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getCreatedDate(), Casbin_.createdDate));
            }
            if (criteria.getModifiedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getModifiedDate(), Casbin_.modifiedDate));
            }
            if (criteria.getDeletedDate() != null) {
                specification = specification.and(buildRangeSpecification(criteria.getDeletedDate(), Casbin_.deletedDate));
            }
            if (criteria.getTbRoleId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbRoleId(),
                    root -> root.join(Casbin_.tbRole, JoinType.LEFT).get(TBRole_.id)));
            }
            if (criteria.getTbFunctionId() != null) {
                specification = specification.and(buildSpecification(criteria.getTbFunctionId(),
                    root -> root.join(Casbin_.tbFunction, JoinType.LEFT).get(TBFunction_.id)));
            }
            if (criteria.getCreatedById() != null) {
                specification = specification.and(buildSpecification(criteria.getCreatedById(),
                    root -> root.join(Casbin_.createdBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getModifiedById() != null) {
                specification = specification.and(buildSpecification(criteria.getModifiedById(),
                    root -> root.join(Casbin_.modifiedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
            if (criteria.getDeletedById() != null) {
                specification = specification.and(buildSpecification(criteria.getDeletedById(),
                    root -> root.join(Casbin_.deletedBy, JoinType.LEFT).get(TBDetailUser_.id)));
            }
        }
        return specification;
    }

    public List<CasbinDTO> getCasbinByCurrentUserRole(final CasbinCriteria criteria, final Pageable page) {
        log.debug("find by criteria : {}, page: {}", criteria, page);
        UserDTO currentUserDTO = userService.getCurrentUserDTO().get();

        LongFilter roleIdFilter = new LongFilter();
        roleIdFilter.setEquals(currentUserDTO.getRoleId());
        criteria.setTbRoleId(roleIdFilter);

        final Specification<Casbin> specification = createSpecification(criteria);
        return casbinMapper.toDto(casbinRepository.findAll(specification));
    }

    public List<CasbinDTO> getCasbinPermissionDTOS(final Long roleId) {
        final Query casbinRuleQuery = em.createNativeQuery(
            String.format(
                "select tbl.*\n" +
                    " from casbin_rule as tbl\n" +
                    " inner join tb_function on tb_function.id = tbl.tb_function_id \n" +
                    " inner join tb_role on tb_role.id = tbl.tb_role_id \n" +
                    " where tbl.tb_role_id = :roleId"), Casbin.class);
        casbinRuleQuery.setParameter("roleId", roleId);
        final List<Casbin> casbinRules = casbinRuleQuery.getResultList();
        return casbinMapper.toDto(casbinRules);
    }

    public Page<CasbinDTO> findByRoleId(final Long roleId, final Pageable pageable) {
        final List<CasbinDTO> casbinDTOs = getCasbinPermissionDTOS(roleId);
        final int start = Math.toIntExact(pageable.getOffset());
        final int end = Math.toIntExact((start + pageable.getPageSize()) > casbinDTOs.size() ? casbinDTOs.size() : (start + pageable.getPageSize()));
        return new PageImpl<>(casbinDTOs.subList(start, end), pageable, casbinDTOs.size());
    }

    public List<CasbinDTO> getPermissionDTOSByFunctionId(final Long functionId) {
        final Query casbinRuleQuery = em.createNativeQuery(
            String.format(
                "select tbl.*\n" +
                    " from casbin_rule as tbl\n" +
                    " inner join tb_function on tb_function.id = tbl.tb_function_id \n" +
                    " inner join tb_role on tb_role.id = tbl.tb_role_id \n" +
                    " where tbl.tb_function_id = :tbFunctionId"), Casbin.class);
        casbinRuleQuery.setParameter("tbFunctionId", functionId);
        final List<Casbin> casbinRules = casbinRuleQuery.getResultList();
        return casbinMapper.toDto(casbinRules);
    }
}
