package ru.yandex.webmaster3.viewer.http.metrika.data;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import ru.yandex.autodoc.common.doc.annotation.Description;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.Action;
import ru.yandex.webmaster3.core.http.ActionResponse;
import ru.yandex.webmaster3.core.http.ReadAction;
import ru.yandex.webmaster3.core.metrics.Category;
import ru.yandex.webmaster3.core.util.WwwUtil;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCrawlSamplesCHDao;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaStatsByTimeCHDao;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaStatsByTime;

@ReadAction
@Category("metrika")
@Component("/user/host/metrika/stats")
@AllArgsConstructor(onConstructor_ = @Autowired)
@Description("Получения данных для графика метрики")
public class GetMetrikaStatsByTimeAction extends Action<GetMetrikaStatsByTimeRequest,
        GetMetrikaStatsByTimeAction.GetMetrikaResponse> {
    private final MetrikaCrawlSamplesCHDao mdbMetrikaCrawlSamplesCHDao;
    private final MetrikaStatsByTimeCHDao mdbMetrikaStatsByTimeCHDao;


    @Override
    public GetMetrikaResponse process(GetMetrikaStatsByTimeRequest request) throws WebmasterException {
        WebmasterHostId hostId = request.getHostId();
        long counterId = request.getCounterId();
        String domain = WwwUtil.cutWWWAndM(hostId.getPunycodeHostname());

        if (mdbMetrikaCrawlSamplesCHDao.getCounterIds(domain).stream().noneMatch(a -> a == counterId)) {
            return new GetMetrikaIllegalCounterIdResponse();
        }

        List<MetrikaStatsByTime> metrika = mdbMetrikaStatsByTimeCHDao.getMetrika(domain, counterId);

        if (metrika.isEmpty()) {
            new GetMetrikaNormalResponse(Collections.emptyList(), null, null, null);
        }

        Map<LocalDate, MetrikaStatsByTime> mapByTime =
                metrika.stream().collect(Collectors.toMap(MetrikaStatsByTime::getDate, Function.identity()));
        List<GetMetrikaNormalResponse.MetrikaPoint> curWeekStats = new ArrayList<>();
        List<GetMetrikaNormalResponse.MetrikaPoint> prevWeekStats = new ArrayList<>();

        LocalDate weekEnd = metrika.get(0).getTableDate();
        LocalDate weekStart = weekEnd.minusWeeks(1);

        for (int i = 1; i <= 7; i++) {
            LocalDate date = weekStart.plusDays(i);
            curWeekStats.add(metrikaToPoint(mapByTime.get(date), date));
        }

        for (int i = 0; i < 7; i++) {
            LocalDate date = weekStart.minusDays(i);
            prevWeekStats.add(metrikaToPoint(mapByTime.get(date), date));
        }

        return new GetMetrikaNormalResponse(
                curWeekStats,
                weekStart,
                weekEnd,
                calcStatsDynamic(prevWeekStats, curWeekStats)
        );
    }

    @NotNull
    private GetMetrikaNormalResponse.MetrikaPoint metrikaToPoint(@Nullable MetrikaStatsByTime stats, LocalDate date) {
        if (stats != null) {
            return new GetMetrikaNormalResponse.MetrikaPoint(date, stats.getBounceRate());
        } else {
            return new GetMetrikaNormalResponse.MetrikaPoint(date, 0d);
        }
    }


    private GetMetrikaNormalResponse.Stats calcStatsDynamic(
            List<GetMetrikaNormalResponse.MetrikaPoint> prevWeekStats,
            List<GetMetrikaNormalResponse.MetrikaPoint> curWeekStats
    ) {
        double prevBounceCount =
                prevWeekStats.stream().mapToDouble(GetMetrikaNormalResponse.MetrikaPoint::getValue).sum();
        double curBounceCount =
                curWeekStats.stream().mapToDouble(GetMetrikaNormalResponse.MetrikaPoint::getValue).sum();

        return new GetMetrikaNormalResponse.Stats(
                (long) (curBounceCount * 100d / 7d),
                prevBounceCount == 0 ? null : (long) (curBounceCount * 100d / prevBounceCount) - 100
        );
    }

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    static abstract class GetMetrikaResponse implements ActionResponse {
        private List<MetrikaPoint> metrikaStatsByTime;
        private LocalDate dateFrom, dateTo;
        private Stats dynamic;

        @Getter
        @AllArgsConstructor
        static class MetrikaPoint {
            private final LocalDate time;
            private final Double value;
        }

        @Getter
        @AllArgsConstructor
        static class Stats {
            private final Long bounceSum;
            private final Long bounceDiff;
        }
    }

    private static class GetMetrikaNormalResponse extends GetMetrikaResponse implements ActionResponse.NormalResponse {
        public GetMetrikaNormalResponse(List<MetrikaPoint> metrikaStatsByTime, LocalDate from, LocalDate to,
                                        Stats stats) {
            super(metrikaStatsByTime, from, to, stats);
        }
    }

    public static class GetMetrikaIllegalCounterIdResponse extends GetMetrikaResponse implements ActionResponse.ErrorResponse {
        @Override
        public Enum<?> getCode() {
            return ErrorType.ILLEGAL_COUNTER_ID;
        }

        @Override
        public String getMessage() {
            return "Illegal counter id";
        }
    }

    enum ErrorType {
        ILLEGAL_COUNTER_ID
    }
}
