package ru.kayashov.bar.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Session;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import ru.kayashov.bar.controller.dto.cocktail.CocktailFilterRequestDto;
import ru.kayashov.bar.controller.dto.cocktail.CocktailForListResponseDto;
import ru.kayashov.bar.controller.dto.cocktail.CocktailModalDto;
import ru.kayashov.bar.controller.dto.cocktail.CocktailSimpleResponseDto;
import ru.kayashov.bar.controller.dto.cocktail.ReceiptResponseDto;
import ru.kayashov.bar.mapper.CocktailMapper;
import ru.kayashov.bar.model.entity.AlcoholicEntity;
import ru.kayashov.bar.model.entity.BarEntity;
import ru.kayashov.bar.model.entity.BarIngredientStorage;
import ru.kayashov.bar.model.entity.BarResident;
import ru.kayashov.bar.model.entity.CategoryEntity;
import ru.kayashov.bar.model.entity.CocktailEntity;
import ru.kayashov.bar.model.entity.GlassEntity;
import ru.kayashov.bar.model.entity.IngredientEntity;
import ru.kayashov.bar.model.entity.Rating;
import ru.kayashov.bar.model.entity.ReceiptEntity;
import ru.kayashov.bar.model.entity.StopList;
import ru.kayashov.bar.model.entity.TagEntity;
import ru.kayashov.bar.model.entity.Visitor;
import ru.kayashov.bar.repository.AlcoholicRepository;
import ru.kayashov.bar.repository.CategoryRepository;
import ru.kayashov.bar.repository.CocktailRepository;
import ru.kayashov.bar.repository.GlassRepository;
import ru.kayashov.bar.repository.IngredientRepository;
import ru.kayashov.bar.repository.RatingRepository;
import ru.kayashov.bar.repository.ReceiptRepository;
import ru.kayashov.bar.repository.StopListRepository;
import ru.kayashov.bar.repository.TagRepository;
import ru.kayashov.bar.repository.VisitorsRepository;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class CocktailService {

    @Value("${cocktail.photo.path}")
    private String photoFolder;

    private final StopListRepository stopListRepository;

    private final CocktailMapper mapper;

    private final CocktailRepository repository;
    private final AlcoholicRepository alcoholicRepository;
    private final CategoryRepository categoryRepository;
    private final GlassRepository glassRepository;
    private final TagRepository tagRepository;

    private final ReceiptRepository receiptRepository;
    private final IngredientRepository ingredientRepository;
    private final VisitorsRepository visitorsRepository;
    private final RatingRepository ratingRepository;
    private final VisitorService visitorService;

    private final EntityManager entityManager;

    /**
     * Вывод меню бара
     */
    @Transactional
    public List<CocktailForListResponseDto> getMenu(CocktailFilterRequestDto dto) {
        return mapper.cocktailsToDtoList(criteria(dto), dto.getAll());
    }

    private List<CocktailEntity> criteria(CocktailFilterRequestDto dto) {
        Visitor visitor = visitorService.getCurrentVisitor();

        Session session = entityManager.unwrap(Session.class);
        CriteriaBuilder cb = session.getCriteriaBuilder();
        CriteriaQuery<CocktailEntity> criteriaQuery = cb.createQuery(CocktailEntity.class);
        Root<CocktailEntity> root = criteriaQuery.from(CocktailEntity.class);
        List<Predicate> predicates = new ArrayList<>();

        if (!dto.getAll()) {
            Long barId = visitor.getResidents().stream()
                    .filter(BarResident::getActive)
                    .map(BarResident::getBar)
                    .map(BarEntity::getId)
                    .toList()
                    .get(0);
            List<Long> cocktailIds = getAllowedCocktailIds(barId);
            Predicate pr = root.get("id").in(cocktailIds);
            predicates.add(pr);

            criteriaQuery.distinct(true);

            List<Long> stopListIds = getStopListIds(visitor);
            predicates.add(cb.not(root.get("id").in(stopListIds)));
        }

        if (!dto.getSearch().isEmpty()) {
            String[] search = dto.getSearch().split(" ");
            List<Predicate> in = new ArrayList<>();
            Join<CocktailEntity, ReceiptEntity> receiptJoin = root.join("receipt", JoinType.LEFT);
            for (String s : search) {
                in.add(cb.like(cb.lower(root.get("name")), "%" + s.toLowerCase() + "%"));
                in.add(cb.like(cb.lower(receiptJoin.get("ingredient").get("name")), "%" + s.toLowerCase() + "%"));
            }
            predicates.add(cb.or(in.toArray(new Predicate[0])));
        }

        if (dto.getOnlyFavourite()) {
            List<Long> favouriteCocktailsId = visitor.getRating().stream()
                    .filter(Rating::isFavorite)
                    .map(Rating::getCocktail)
                    .map(CocktailEntity::getId)
                    .toList();
            predicates.add(root.get("id").in(favouriteCocktailsId));
        }

        if (dto.getGlass() != null && !dto.getGlass().isEmpty()) {
            predicates.add(root.get("glassEntity").get("name").in(dto.getGlass()));
        }

        if (dto.getCategory() != null && !dto.getCategory().isEmpty()) {
            predicates.add(root.get("categoryEntity").get("name").in(dto.getCategory()));
        }

        if (dto.getAlcohol() != null && !dto.getAlcohol().isEmpty()) {
            predicates.add(root.get("alcoholicEntity").get("name").in(dto.getAlcohol()));
        }

        if (!dto.getTags().isEmpty()) {
            Join<CocktailEntity, TagEntity> tagJoin = root.join("tags", JoinType.LEFT);
            predicates.add(tagJoin.get("name").in(dto.getTags()));
        }

        if (dto.getInMenu() != null) {
            List<Long> stopListIds = getStopListIds(visitor);
            switch (dto.getInMenu()) {
                case "Есть в меню" -> predicates.add(cb.not(root.get("id").in(stopListIds)));
                case "Нет в меню" -> predicates.add(root.get("id").in(stopListIds));
            }
        }

        if(dto.getICount() != null) {
            log.info("iii");
        }
//
//        if (!dto.getSortField().equals("name") || !dto.getSortOrder().equals("asc")) {
//            cb.asc(root.get("name"));
////            query.orderBy(cb.asc(root.get("name").get(dto.getSortField())));
//        } else {
//            criteriaQuery.orderBy(cb.asc(root.get("name")));
//        }

        //todo: доделать другие виды сортировки
        Order order;
        switch (dto.getSort()) {
            case NAME_ASC -> order = cb.asc(root.get("name"));
            case NAME_DESC -> order = cb.desc(root.get("name"));
            default -> order = cb.asc(root.get("name"));
        }

        criteriaQuery.where(predicates.toArray(new Predicate[0]))
                .orderBy(order);
        Query<CocktailEntity> query = session.createQuery(criteriaQuery);
        query.setFirstResult(dto.getPage() * dto.getSize());
        query.setMaxResults(dto.getSize());
        List<CocktailEntity> cocktailEntities = query.getResultList();

        log.info("Найдено {} коктейлей", cocktailEntities.size());
//        return Collections.emptyList();
        return cocktailEntities;
    }

    private List<Long> getStopListIds(Visitor visitor) {
        return visitor.getResidents()
                .stream()
                .filter(BarResident::getActive)
                .map(BarResident::getBar)
                .map(BarEntity::getStops)
                .flatMap(List::stream)
                .map(StopList::getCocktail)
                .map(CocktailEntity::getId)
                .toList();
    }

    private List<Long> getAllowedCocktailIds(Long barId) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> query = cb.createQuery(Long.class);

        Root<ReceiptEntity> receiptRoot = query.from(ReceiptEntity.class);
        Join<ReceiptEntity, IngredientEntity> ingredientJoin = receiptRoot.join("ingredient", JoinType.LEFT);
        Join<IngredientEntity, BarIngredientStorage> barIngredientStorageJoin = ingredientJoin.join("barIngredients", JoinType.LEFT);

        // Внешний подзапрос с NOT EXISTS
        Subquery<Long> subquery = query.subquery(Long.class);
        Root<ReceiptEntity> receiptSubRoot = subquery.from(ReceiptEntity.class);
        Join<ReceiptEntity, IngredientEntity> ingredientSubJoin = receiptSubRoot.join("ingredient", JoinType.LEFT);

        // Внутренний подзапрос с NOT EXISTS
        Subquery<Long> innerSubquery = subquery.subquery(Long.class);
        Root<BarIngredientStorage> barIngredientStorageInnerRoot = innerSubquery.from(BarIngredientStorage.class);

        // Условия внутреннего подзапроса
        innerSubquery.select(barIngredientStorageInnerRoot.get("id"))
                .where(
                        cb.equal(barIngredientStorageInnerRoot.get("ingredient"), ingredientSubJoin.get("id")),
                        cb.equal(barIngredientStorageInnerRoot.get("bar").get("id"), barId)
                );

        // Условия внешнего подзапроса
        subquery.select(receiptSubRoot.get("id"))
                .where(
                        cb.equal(receiptSubRoot.get("cocktail").get("id"), receiptRoot.get("cocktail").get("id")),
                        cb.not(cb.exists(innerSubquery))
                );

        // Основной запрос
        query.select(receiptRoot.get("cocktail").get("id"))
                .distinct(true)
                .where(
                        cb.equal(barIngredientStorageJoin.get("bar").get("id"), barId),
                        cb.not(cb.exists(subquery))
                );

        return entityManager.createQuery(query).getResultList();
    }

    public CocktailForListResponseDto findById(Long id) {
        CocktailEntity cocktail = repository.findById(id).orElseThrow();
        return mapper.cocktailToFullDto(cocktail);
    }

    @Transactional
    public void edit(CocktailForListResponseDto dto) {
        CocktailEntity cocktail;
        if (dto.getId() == null) {
            if (dto.getName() == null) {
                throw new RuntimeException("Коктейль не может быть без названия");
            }
            cocktail = new CocktailEntity();
            cocktail.setReceipt(new ArrayList<>());
        } else {
            cocktail = repository.findById(dto.getId())
                    .orElseThrow(() -> new RuntimeException("Не удалось найти коктейль с id " + dto.getId()));
        }
        CategoryEntity category = categoryRepository.findByNameIgnoreCase(dto.getCategory())
                .orElseThrow(() -> new RuntimeException("Не удалось найти категорию " + dto.getCategory()));
        GlassEntity glassEntity = glassRepository.findByNameIgnoreCase(dto.getGlass())
                .orElseThrow(() -> new RuntimeException("Не удалось найти посуду" + dto.getGlass()));
        AlcoholicEntity alcoholicEntity = alcoholicRepository.findByName(dto.getAlcoholic())
                .orElseThrow(() -> new RuntimeException("Не удалось найти алкогольность" + dto.getAlcoholic()));

        cocktail.setName(dto.getName());
        cocktail.setInstructions(dto.getInstructions());
        cocktail.setImage(dto.getImage());
        cocktail.setVideo(cocktail.getVideo());
        cocktail.setCategoryEntity(category);
        cocktail.setGlassEntity(glassEntity);
        cocktail.setAlcoholicEntity(alcoholicEntity);
        cocktail.setTags(findTags(dto.getTags()));
        cocktail.setRating(cocktail.getRating());
        repository.save(cocktail);

        editCocktailReceipts(cocktail.getReceipt(), dto.getReceipt(), cocktail);
    }

    public void editFavourite(Long cocktailId, Long visitorId, boolean put) {
        Visitor visitor = visitorsRepository.findById(visitorId).orElseThrow();

        Optional<Rating> ratingOpt = ratingRepository.findRatingByCocktailIdAndVisitorId(cocktailId, visitorId);

        Rating rating;
        if (put) {
            CocktailEntity cocktail = repository.findById(cocktailId)
                    .orElseThrow();
            if (ratingOpt.isEmpty()) {
                rating = new Rating();
                rating.setCocktail(cocktail);
                rating.setVisitor(visitor);
            } else {
                rating = ratingOpt.get();
            }
            rating.setFavorite(true);
            ratingRepository.save(rating);
        } else {
            if (ratingOpt.isPresent()) {
                rating = ratingOpt.get();
                rating.setFavorite(false);
                ratingRepository.save(rating);
            }
        }
    }

    public void setRating(Long cocktailId, Integer rating) {
        Long id = ((Visitor) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId();
        Visitor visitor = visitorsRepository.findById(id)
                .orElseThrow();

        Rating rate;
        Optional<Rating> rateOpt = ratingRepository.findRatingByCocktailIdAndVisitorId(cocktailId, id);
        if (rateOpt.isEmpty()) {
            rate = new Rating();
            rate.setVisitor(visitor);
            rate.setCocktail(repository.findById(cocktailId).orElseThrow());
            rate.setFavorite(false);
        } else {
            rate = rateOpt.get();
        }

        rate.setRating(rating);
        ratingRepository.save(rate);
    }

    private void editCocktailReceipts(List<ReceiptEntity> old, List<ReceiptResponseDto> actual, CocktailEntity cocktail) {
        for (ReceiptResponseDto receipt : actual) {
            if (receipt.getId() == null) {
                createNewReceipt(receipt, cocktail);
                continue;
            }
            old.stream().filter(r -> receipt.getId().equals(r.getId()))
                    .findFirst()
                    .ifPresent(r -> equalReceipt(r, receipt));

            old.removeIf(r -> r.getId().equals(receipt.getId()));
        }

        old.forEach(r -> receiptRepository.deleteById(r.getId()));
    }

    private void equalReceipt(ReceiptEntity old, ReceiptResponseDto actual) {
        boolean needSave = false;
        if (!actual.getIngredient().getId().equals(old.getIngredient().getId())) {
            IngredientEntity ingredient = ingredientRepository.findById(actual.getIngredient().getId()).orElseThrow();
            old.setIngredient(ingredient);
            needSave = true;
        }
        if (old.getUnit() == null || actual.getUnit() == null ||
                !Objects.equals(old.getUnit().getId(), actual.getUnit().getId())) {
            old.setUnit(actual.getUnit());
            needSave = true;
        }
        if (!Objects.equals(old.getCount(), actual.getCount())) {
            old.setCount(actual.getCount());
            needSave = true;
        }
        if (needSave) {
            receiptRepository.save(old);
        }
    }

    private void createNewReceipt(ReceiptResponseDto receipt, CocktailEntity cocktail) {
        ReceiptEntity receiptEntity = new ReceiptEntity();
        IngredientEntity ingredient = ingredientRepository.findById(receipt.getIngredient().getId()).orElseThrow();
        receiptEntity.setIngredient(ingredient);
        receiptEntity.setCount(receipt.getCount());
        receiptEntity.setUnit(receipt.getUnit());
//        receiptEntity.setMeasure(receipt.getMeasure());
        receiptEntity.setCocktail(cocktail);
        receiptRepository.save(receiptEntity);
    }

    private List<TagEntity> findTags(String tagString) {
        if (tagString == null || tagString.isEmpty()) {
            return new ArrayList<>();
        }
        List<TagEntity> tags = new ArrayList<>();
        for (String name : tagString.split(",")) {
            if (name.isEmpty()) {
                continue;
            }
            TagEntity tagEntity = tagRepository.findByName(name)
                    .orElseThrow(() -> new RuntimeException("Не удалось найти тег " + name));
            tags.add(tagEntity);
        }
        return tags;
    }

    public void inMenuEdit(Long id, Boolean value) {
        Visitor visitor = visitorService.getCurrentVisitor();
        CocktailEntity entity = repository.findById(id)
                .orElseThrow(() -> new RuntimeException("Не удалось найти коктейль с id " + id));

        BarEntity bar = visitor.getResidents().stream()
                .filter(BarResident::getActive)
                .map(BarResident::getBar)
                .toList()
                .get(0);

        Optional<StopList> stop = bar.getStops().stream()
                .filter(s -> Objects.equals(s.getCocktail().getId(), id))
                .findFirst();

        if (value && stop.isPresent()) {
            stopListRepository.deleteById(stop.get().getId());
            return;
        }

        if (!value && stop.isEmpty()) {
            StopList stopList = new StopList();
            stopList.setCocktail(entity);
            stopList.setBar(bar);
            stopListRepository.save(stopList);
        }
    }

    public CocktailModalDto getForModal(Long id) {
        CocktailEntity cocktail = repository.findById(id).orElseThrow(RuntimeException::new);
        return mapper.cocktailToModalDto(cocktail);
    }

    public List<ReceiptResponseDto> getReceipts(Long id) {

        return null;
    }

    public List<CocktailSimpleResponseDto> getSimple() {
        return repository.findAll().stream()
                .map(CocktailSimpleResponseDto::mapToDto)
                .toList();
    }


    public String savePhoto(MultipartFile file) throws IOException {
        File folder = new File(photoFolder);
        List<File> files = Arrays.asList(Objects.requireNonNull(folder.listFiles()));
        String fileName = getPhotoPath(files, file.getOriginalFilename());
        String fullName = photoFolder + "/" + fileName;
        file.transferTo(new File(fullName));
        log.info("сохранено фото {}", fileName);
        return "/assets/cocktails/" + fileName;
    }

    private String getPhotoPath(List<File> files, String originalName) {
        if (files.stream().map(File::getName).anyMatch(name -> name.equals(originalName))) {
            String[] split = originalName.split("\\.");
            String name = concatName(split) + "_c." + split[split.length - 1];
            return getPhotoPath(files, name);
        }
        return originalName;
    }

    private String concatName(String[] name) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < name.length - 1; i++) {
            sb.append(name[i]);
        }
        return sb.toString();
    }

    public void delete(Long id) {
        CocktailEntity cocktail = repository.findById(id).orElseThrow(RuntimeException::new);
        log.info("Удален коктейль {}", cocktail);
        repository.delete(cocktail);
    }
}
