package ru.yandex.webmaster3.worker.review;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Range;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.storage.events.data.WMCEvent;
import ru.yandex.webmaster3.storage.events.data.events.RetranslateToUsersEvent;
import ru.yandex.webmaster3.storage.events.data.events.UserHostMessageEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.dao.CommonDataStateYDao;
import ru.yandex.webmaster3.storage.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
import ru.yandex.webmaster3.storage.util.yt.YtCypressService;
import ru.yandex.webmaster3.storage.util.yt.YtException;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtService;
import ru.yandex.webmaster3.storage.util.yt.YtTableReadDriver;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

/**
 * ishalaru
 * 24.12.2019
 **/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
@Component("importReviewTask")
public class ImportReviewTask extends PeriodicTask<ImportReviewTask.State> {
    private static final String REVIEW_TYPE = "/ugc/review";
    private static final int MAX_MESSAGES_PACK = 5;
    private final YtService ytService;
    private final CommonDataStateYDao commonDataStateYDao;
    private final WMCEventsService wmcEventsService;

    @Value("${webmaster3.worker.review.import.yt.path}")
    private YtPath path;


    @Override
    public Result run(UUID runId) throws Exception {
        log.info("Start import reviews.");
        setState(new State());
        state.notificationByType = new HashMap<>();
        final LocalDate currentDate = LocalDate.now();
        final LocalDate value = Optional.ofNullable(commonDataStateYDao.getValue(CommonDataType.LAST_IMPORT_REVIEW_DATE)).
                map(state -> LocalDate.parse(state.getValue()))
                .orElse(LocalDate.now().minusDays(1));
        if (!currentDate.isAfter(value)) {
            log.info("Import reviews processed early.");
            return Result.SUCCESS;
        }
        final LocalDate processingTime = LocalDate.now().minusDays(1);
        Map<WebmasterHostId, ReviewCommentContainer> map = new HashMap<>();
        ytService.inTransaction(path).execute(cypressService -> {
            readTable(cypressService, path, processingTime, map);
            return true;
        });
        if (!map.isEmpty()) {
            // otherwise retry later
            commonDataStateYDao.update(new CommonDataState(CommonDataType.LAST_IMPORT_REVIEW_DATE, currentDate.toString(), new DateTime()));
        }
        for (Map.Entry<WebmasterHostId, ReviewCommentContainer> item : map.entrySet()) {
            final Set<WebmasterHostId> webmasterHostIds = new HashSet<>(IdUtils.allHostsForDomain(item.getKey()));
            for (WebmasterHostId webmasterHostId : webmasterHostIds) {
                state.countNotifiedHosts++;
                state.getNotificationByType().compute("TYPE_2", (key, val) -> (val == null) ? 1 : val + 1);
                List<WMCEvent> sendList = new ArrayList<>();
                final WMCEvent message = createMessage("TYPE_2", webmasterHostId, item.getValue());
                sendList.add(message);
                wmcEventsService.addEvents(sendList);
            }
        }

        log.info("Finished import reviews.");
        return Result.SUCCESS;
    }

    private WMCEvent createMessage(String renderType, WebmasterHostId hostId, ReviewCommentContainer reviewCommentContainer) {
        return WMCEvent.create(new RetranslateToUsersEvent<>(
                new UserHostMessageEvent<>(
                        hostId,
                        null,
                        new MessageContent.NewReviewAvailable(
                                hostId,
                                reviewCommentContainer.messages,
                                reviewCommentContainer.countPositive,
                                reviewCommentContainer.countNegative,
                                renderType),
                        NotificationType.NEW_REVIEW_AVAILABLE,
                        false, null),
                List.of()
        ));
    }

    private void readTable(YtCypressService cypressService, YtPath path, LocalDate currentDate, Map<WebmasterHostId, ReviewCommentContainer> map) {
        AsyncTableReader<ReviewRow> tableReader = new AsyncTableReader<>(cypressService, path, Range.all(),
                YtTableReadDriver.createYSONDriver(ReviewRow.class, JsonMapping.OM))
                .splitInParts(20000L)
                .withThreadName("review-loader-cacher")
                .withRetry(5);

        DateTime startDateTime = currentDate.toDateTimeAtStartOfDay(DateTimeZone.UTC);
        DateTime endDateTime = currentDate.plusDays(1).toDateTimeAtStartOfDay(DateTimeZone.UTC);
        String lastObjectId = "";
        WebmasterHostId lastHostId = null;
        try (AsyncTableReader.TableIterator<ReviewRow> it = tableReader.read()) {
            while (it.hasNext()) {
                try {
                    state.totalReviewCount++;
                    final ReviewRow next = it.next();
                    if (!lastObjectId.equals(next.objectId)) {
                        lastObjectId = next.objectId;
                        lastHostId = next.getHostId();
                    }
                    final Review review = new Review(lastHostId, next.review);
                    if (!review.reviewComment.getType().equals(REVIEW_TYPE) || !checkReview(startDateTime, endDateTime, review)) {
                        continue;
                    }
                    state.newReviewCount++;
                    final ReviewCommentContainer reviewInfos = map.computeIfAbsent(review.getHostId(), k -> new ReviewCommentContainer());
                    reviewInfos.addReviewComment(review.reviewComment);
                } catch (Exception exp) {
                    lastObjectId = "";
                    lastHostId = null;
                    log.debug(exp.getMessage(), exp);
                }
            }
        } catch (InterruptedException | IOException e) {
            throw new YtException("Unable to read table: " + path, e);
        }
    }

    private boolean checkReview(DateTime startDate, DateTime endDate, Review review) {
        final Long time = Long.valueOf(review.reviewComment.getTime());
        return !review.getReviewComment().getMeta().blocked && review.getReviewComment().getMeta().moderated &&
                startDate.getMillis() <= time && time < endDate.getMillis();
    }


    public static class ReviewCommentContainer {
        @Getter
        int countNegative;
        @Getter
        int countPositive;
        @Getter
        List<String> messages = new ArrayList<>(MAX_MESSAGES_PACK);

        public void addReviewComment(ReviewComment reviewComment) {
            if (reviewComment.ratingInfo.val != null) {
                if (reviewComment.ratingInfo.val <= 3.1) {
                    countNegative++;
                } else {
                    countPositive++;
                }
            }
            if (messages.size() < MAX_MESSAGES_PACK) {
                messages.add(reviewComment.text);
            }
        }
    }

    @lombok.Value
    @Builder
    public static class Review {
        WebmasterHostId hostId;
        ReviewComment reviewComment;
    }

    @lombok.Value
    public static class ReviewRow {
        @JsonProperty("object_id")
        String objectId;
        @JsonProperty("yson")
        ReviewComment review;
        @JsonProperty("user_id")
        String userId;

        private WebmasterHostId getHostId() {
            final String[] split = objectId.split("/");
            if (split.length < 3) {
                throw new IllegalArgumentException("Incorrect id name.");
            }

            String domain = new String(Base64.getDecoder().decode(split[2]));
            return IdUtils.urlToHostId(domain);
        }

    }

    @lombok.Value
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class RatingInfo {
        @JsonProperty("Max")
        Integer max;
        @JsonProperty("Val")
        Double val;
    }

    @lombok.Value
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Meta {
        @JsonProperty("Blocked")
        Boolean blocked;
        @JsonProperty("Moderated")
        Boolean moderated;
    }

    @lombok.Value
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class ReviewComment {
        @JsonProperty("Id")
        String id;
        @JsonProperty("Meta")
        Meta meta;
        @JsonProperty("Rating")
        RatingInfo ratingInfo;
        @JsonProperty("Text")
        String text;
        @JsonProperty("Time")
        String time;
        @JsonProperty("Type")
        String type;
    }

    @Override
    public PeriodicTaskType getType() {
        return PeriodicTaskType.IMPORT_REVIEW;
    }

    @Override
    public TaskSchedule getSchedule() {
        return TaskSchedule.startByCron("0 55 */6 * * *");
    }


    public static final class State implements PeriodicTaskState {
        @Getter
        long totalReviewCount;
        @Getter
        long newReviewCount;
        @Getter
        long countNotifiedHosts;
        @Getter
        Map<String, Long> notificationByType;
    }
}
