package com.mapr.music.service;

import com.mapr.music.dao.*;
import com.mapr.music.dto.RateDto;
import com.mapr.music.exception.ResourceNotFoundException;
import com.mapr.music.model.*;
import org.apache.commons.beanutils.PropertyUtilsBean;

import javax.inject.Inject;
import javax.inject.Named;
import java.lang.reflect.InvocationTargetException;
import java.security.Principal;
import java.util.List;
import java.util.UUID;

public class RateService {

    private final AlbumRateDao albumRateDao;
    private final ArtistRateDao artistRateDao;
    private final AlbumDao albumDao;
    private final ArtistDao artistDao;
    private final MaprDbDao<User> userDao;

    @Inject
    public RateService(AlbumRateDao albumRateDao,
                       ArtistRateDao artistRateDao,
                       @Named("albumDao") AlbumDao albumDao,
                       @Named("artistDao") ArtistDao artistDao,
                       @Named("userDao") MaprDbDao<User> userDao) {

        this.albumRateDao = albumRateDao;
        this.artistRateDao = artistRateDao;
        this.albumDao = albumDao;
        this.artistDao = artistDao;
        this.userDao = userDao;
    }

    /**
     * Returns User's rate for the specified Album.
     *
     * @param user    user's principal.
     * @param albumId album's identifier.
     * @return User's rate for the specified Album. Empty rate will be returned in case when there is no such rate.
     */
    public RateDto getAlbumRate(Principal user, String albumId) {

        if (user == null || user.getName() == null || user.getName().isEmpty()) {
            return new RateDto();
        }

        String userId = user.getName();
        if (!userDao.exists(userId)) {
            throw new ResourceNotFoundException("User with id '" + userId + "' not found");
        }

        if (!albumDao.exists(albumId)) {
            throw new ResourceNotFoundException("Album with id '" + albumId + "' not found");
        }

        AlbumRate rate = albumRateDao.getRate(userId, albumId);
        return (rate != null) ? albumRateToDto(rate) : new RateDto();
    }


    /**
     * Allows to rate specified Album on behalf of specified User.
     *
     * @param userId    user's identifier.
     * @param albumId   album's identifier.
     * @param albumRate rate Data Transfer Object.
     * @return created or updated Album's rate.
     */
    public RateDto rateAlbum(String userId, String albumId, RateDto albumRate) {

        if (albumRate == null) {
            throw new IllegalArgumentException("Album rate can not be null");
        }

        return rateAlbum(userId, albumId, albumRate.getRating());
    }

    /**
     * Allows to rate specified Album on behalf of specified User.
     *
     * @param userId  user's identifier.
     * @param albumId album's identifier.
     * @param rate    rate.
     * @return created or updated Album's rate.
     */
    public RateDto rateAlbum(String userId, String albumId, double rate) {

        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("User id can not be empty");
        }

        if (albumId == null || albumId.isEmpty()) {
            throw new IllegalArgumentException("Album id can not be empty");
        }

        if (!userDao.exists(userId)) {
            throw new ResourceNotFoundException("User with id '" + userId + "' not found");
        }

        Album existingAlbum = albumDao.getById(albumId);
        if (existingAlbum == null) {
            throw new ResourceNotFoundException("Album with id '" + albumId + "' not found");
        }

        AlbumRate possibleExistingRate = albumRateDao.getRate(userId, albumId);
        if (possibleExistingRate != null) {
            possibleExistingRate.setRating(rate);
            AlbumRate newAlbumRate = albumRateDao.update(possibleExistingRate.getId(), possibleExistingRate);

            return recomputeAlbumRate(newAlbumRate, existingAlbum);
        }

        AlbumRate albumRate = new AlbumRate();
        albumRate.setId(UUID.randomUUID().toString());
        albumRate.setUserId(userId);
        albumRate.setDocumentId(albumId);
        albumRate.setRating(rate);
        AlbumRate newAlbumRate = albumRateDao.create(albumRate);

        return recomputeAlbumRate(newAlbumRate, existingAlbum);
    }

    private RateDto recomputeAlbumRate(AlbumRate newAlbumRate, Album existingAlbum) {
        List<AlbumRate> albumRates = albumRateDao.getByAlbumId(newAlbumRate.getDocumentId());
        double aggregatedRating = albumRates.stream().mapToDouble(AlbumRate::getRating).sum() / albumRates.size();
        existingAlbum.setRating(aggregatedRating);
        albumDao.update(existingAlbum.getId(), existingAlbum);

        return new RateDto(aggregatedRating);
    }

    private RateDto recomputeArtistRate(ArtistRate newArtistRate, Artist existingArtist) {
        List<ArtistRate> artistRates = artistRateDao.getByArtistId(newArtistRate.getDocumentId());
        double aggregatedRating = artistRates.stream().mapToDouble(ArtistRate::getRating).sum() / artistRates.size();
        existingArtist.setRating(aggregatedRating);
        artistDao.update(existingArtist.getId(), existingArtist);

        return new RateDto(aggregatedRating);
    }

    /**
     * Allows to rate specified Album on behalf of specified User.
     *
     * @param user      user's principal.
     * @param albumId   album's identifier.
     * @param albumRate rate Data Transfer Object.
     * @return created or updated Album's rate.
     */
    public RateDto rateAlbum(Principal user, String albumId, RateDto albumRate) {

        if (user == null) {
            throw new IllegalArgumentException("User principal can not be null");
        }

        if (albumRate == null) {
            throw new IllegalArgumentException("Album rate can not be null");
        }

        return rateAlbum(user.getName(), albumId, albumRate.getRating());
    }

    /**
     * Returns User's rate for the specified Artist.
     *
     * @param user     user's principal.
     * @param artistId artists's identifier.
     * @return User's rate for the specified Artist. Empty rate will be returned in case when there is no such rate.
     */
    public RateDto getArtistRate(Principal user, String artistId) {

        if (user == null || user.getName() == null || user.getName().isEmpty()) {
            return new RateDto();
        }

        String userId = user.getName();
        if (!userDao.exists(userId)) {
            throw new ResourceNotFoundException("User with id '" + userId + "' not found");
        }

        if (!artistDao.exists(artistId)) {
            throw new ResourceNotFoundException("Artist with id '" + artistId + "' not found");
        }

        ArtistRate rate = artistRateDao.getRate(userId, artistId);
        return (rate != null) ? artistRateToDto(rate) : new RateDto();
    }

    /**
     * Allows to rate specified Artist on behalf of specified User.
     *
     * @param userId     user's identifier.
     * @param artistId   artist's identifier.
     * @param artistRate rate Data Transfer Object.
     * @return created or updated Artist's rate.
     */
    public RateDto rateArtist(String userId, String artistId, RateDto artistRate) {

        if (artistRate == null) {
            throw new IllegalArgumentException("Artist rate can not be null");
        }

        return rateArtist(userId, artistId, artistRate.getRating());
    }

    /**
     * Allows to rate specified Artist on behalf of specified User.
     *
     * @param userId   user's identifier.
     * @param artistId artist's identifier.
     * @param rate     rate.
     * @return created or updated Artist's rate.
     */
    public RateDto rateArtist(String userId, String artistId, double rate) {

        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("User id can not be empty");
        }

        if (artistId == null || artistId.isEmpty()) {
            throw new IllegalArgumentException("Artist id can not be empty");
        }

        if (!userDao.exists(userId)) {
            throw new ResourceNotFoundException("User with id '" + userId + "' not found");
        }

        Artist existingArtist = artistDao.getById(artistId);
        if (existingArtist == null) {
            throw new ResourceNotFoundException("Artist with id '" + artistId + "' not found");
        }

        ArtistRate possibleExistingRate = artistRateDao.getRate(userId, artistId);
        if (possibleExistingRate != null) {
            possibleExistingRate.setRating(rate);
            ArtistRate newArtistRate = artistRateDao.update(possibleExistingRate.getId(), possibleExistingRate);

            return recomputeArtistRate(newArtistRate, existingArtist);
        }

        ArtistRate artistRate = new ArtistRate();
        artistRate.setId(UUID.randomUUID().toString());
        artistRate.setUserId(userId);
        artistRate.setDocumentId(artistId);
        artistRate.setRating(rate);

        ArtistRate newArtistRate = artistRateDao.create(artistRate);

        return recomputeArtistRate(newArtistRate, existingArtist);
    }

    /**
     * Allows to rate specified Artist on behalf of specified User.
     *
     * @param user       user's principal.
     * @param artistId   artist's identifier.
     * @param artistRate rate Data Transfer Object.
     * @return created or updated Artist's rate.
     */
    public RateDto rateArtist(Principal user, String artistId, RateDto artistRate) {

        if (user == null) {
            throw new IllegalArgumentException("User principal can not be null");
        }

        if (artistRate == null) {
            throw new IllegalArgumentException("Artist rate can not be null");
        }

        return rateArtist(user.getName(), artistId, artistRate.getRating());
    }

    private RateDto artistRateToDto(ArtistRate artistRate) {

        RateDto rateDto = new RateDto();
        PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
        try {
            propertyUtilsBean.copyProperties(rateDto, artistRate);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException("Can not create artist rate Data Transfer Object", e);
        }

        return rateDto;
    }

    private RateDto albumRateToDto(AlbumRate albumRate) {

        RateDto rateDto = new RateDto();
        PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
        try {
            propertyUtilsBean.copyProperties(rateDto, albumRate);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException("Can not create album rate Data Transfer Object", e);
        }

        return rateDto;
    }

}