package ru.yandex.webmaster3.worker.digest.notificationsettings;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.MutableLong;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.notification.LanguageEnum;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.util.concurrent.graph.BlockingBatchConsumer;
import ru.yandex.webmaster3.core.util.concurrent.graph.FailPolicy;
import ru.yandex.webmaster3.core.util.concurrent.graph.GraphExecution;
import ru.yandex.webmaster3.core.util.concurrent.graph.GraphExecutionBuilder;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;
import ru.yandex.webmaster3.storage.notifications.NotificationChannel;
import ru.yandex.webmaster3.storage.user.UserPersonalInfo;
import ru.yandex.webmaster3.storage.user.dao.VerifiedHostsYDao;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.util.yt.TableWriter;
import ru.yandex.webmaster3.worker.notifications.info.UserHostsChannelsInfo;
import ru.yandex.webmaster3.worker.notifications.info.UserHostsInfo;
import ru.yandex.webmaster3.worker.notifications.info.UserHostsPersonalsInfo;
import ru.yandex.webmaster3.worker.notifications.services.ChannelsResolver;
import ru.yandex.webmaster3.worker.notifications.services.EmailsResolver;
import ru.yandex.webmaster3.worker.notifications.services.PersonalsResolver;

/**
 * Created by ifilippov5 on 29.08.17.
 */
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UserNotificationSettingsBuildService {

    private final VerifiedHostsYDao verifiedHostsYDao;
    private final ChannelsResolver channelsResolver;
    private final EmailsResolver emailsResolver;
    private final PersonalsResolver personalsResolver;
    private final UserNotificationSettingsMetricsService userNotificationSettingsMetricsService;

    public void upload(final TableWriter tw) {
        UserNotificationSettingsMetricsAcc stats = userNotificationSettingsMetricsService.createNonThreadSafeAccumulator();

        GraphExecutionBuilder builder = GraphExecutionBuilder.newBuilder("upload-user-settings");
        GraphExecutionBuilder.Queue<UserHostsPersonalsInfo> writer = builder
                .process(() -> uploadOnYt(tw))
                .name("uploader")
                .batchLimit(10000)
                .getInput();

        GraphExecutionBuilder.Queue<UserHostsChannelsInfo> bbResolver = builder
                .process(writer, personalsResolver::resolveBlackbox)
                .name("blackbox")
                .batchLimit(100)
                .getInput();

        GraphExecutionBuilder.Queue<UserHostsChannelsInfo> personalsResolver = builder
                .process(writer, bbResolver, this.personalsResolver::resolvePersonals)
                .name("personals")
                .concurrency(4)
                .batchLimit(1000)
                .getInput();

        GraphExecutionBuilder.Queue<UserHostsChannelsInfo> statsCollector = builder
                .process(() -> BlockingBatchConsumer.perItem(RetryUtils.never(), FailPolicy.THROW, stats::accountUserStats))
                .name("metrics")
                .concurrency(1)
                .getInput();

        GraphExecutionBuilder.Queue<UserHostsChannelsInfo> emailsResolver = builder
                .process(GraphExecutionBuilder.multiplex(personalsResolver, statsCollector), this.emailsResolver::resolve)
                .name("emails")
                .concurrency(4)
                .batchLimit(1000)
                .getInput();

        GraphExecutionBuilder.Queue<UserHostsInfo> channels = builder
                .process(personalsResolver, emailsResolver, (q1, q2) ->
                        channelsResolver.resolve(q1, q2, EnumSet.allOf(NotificationType.class)))
                .name("channels")
                .concurrency(4)
                .batchLimit(1000)
                .getInput();


        GraphExecution<UserHostsInfo> graph = builder.build(channels);

        graph.start();
        try {
            MutableLong curUser = new MutableLong(-1L);
            List<WebmasterHostId> hosts = new ArrayList<>();
            verifiedHostsYDao.forEachUserAndHost(uh -> {
                try {
                    if (!curUser.getValue().equals(uh.getLeft())) {
                        if (!hosts.isEmpty()) {
                            graph.put(new UserHostsInfo(curUser.getValue(), new ArrayList<>(hosts)));
                            hosts.clear();
                        }
                        curUser.setValue(uh.getLeft());
                    }
                    hosts.add(uh.getRight());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            if (!hosts.isEmpty()) {
                graph.put(new UserHostsInfo(curUser.getValue(), new ArrayList<>(hosts)));
            }

            graph.doneWritingAndAwaitTermination();
            userNotificationSettingsMetricsService.sendStats(stats);
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Failed to stream users",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        } catch (InterruptedException | ExecutionException e) {
            throw new WebmasterException("Execution failed",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), e);
        } finally {
            graph.terminateAbruptly();
        }
    }

    private BlockingBatchConsumer<UserHostsPersonalsInfo> uploadOnYt(final TableWriter tw) {
        // не ретраим, так как может случиться только IOException при записи в файл. К тому же, если будем ретраить, то тогда
        // придется чистить tw, в противном случае если транзакция не откатится, то на yt буду отправлены данные с повторениями
        return batch -> {
            for (UserHostsPersonalsInfo info : batch) {
                if (info == null || info.getChannelsInfo() == null || info.getChannelsInfo().getHost2Settings() == null) {
                    continue;
                }
                for (Map.Entry<WebmasterHostId, Map<NotificationType, Set<NotificationChannel>>> entry : info.getChannelsInfo().getHost2Settings().entrySet()) {
                    WebmasterHostId host = entry.getKey();
                    Map<NotificationType, Set<NotificationChannel>> mp = entry.getValue();
                    if (mp == null || mp.isEmpty()) {
                        // нет данных по настройкам нотификаций, поэтому ничего отгружать не будем
                        continue;
                    }
                    for (Map.Entry<NotificationType, Set<NotificationChannel>> channelsEntry : mp.entrySet()) {
                        boolean service = false;
                        boolean email = false;
                        boolean sup = false;
                        NotificationType notification = channelsEntry.getKey();
                        Set<NotificationChannel> channels = channelsEntry.getValue();
                        if (channels != null) {
                            service = channels.contains(NotificationChannel.SERVICE);
                            email = channels.contains(NotificationChannel.EMAIL);
                            sup = channels.contains(NotificationChannel.SUP);
                        }
                        tw.column(YtRow.F_USER_ID, Optional.ofNullable(info.getChannelsInfo())
                                .map(UserHostsChannelsInfo::getUserId)
                                .orElse(-1L));
                        tw.column(YtRow.F_LOGIN, Optional.ofNullable(info.getUserPersonalInfo())
                                .map(UserPersonalInfo::getLogin)
                                .orElse(null));
                        tw.column(YtRow.F_EMAIL, Optional.ofNullable(info.getChannelsInfo())
                                .map(UserHostsChannelsInfo::getEmail)
                                .orElse(null));
                        tw.column(YtRow.F_LANGUAGE, Optional.ofNullable(info.getUserPersonalInfo())
                                .map(UserPersonalInfo::getLanguage)
                                .map(LanguageEnum::name)
                                .orElse(null));
                        tw.column(YtRow.F_FIO, Optional.ofNullable(info.getUserPersonalInfo())
                                .map(UserPersonalInfo::getFio)
                                .orElse(null));
                        tw.column(YtRow.F_HOST_ID, Optional.ofNullable(host)
                                .map(WebmasterHostId::toString)
                                .orElse(null));
                        tw.column(YtRow.F_NOTIFICATION_TYPE, Optional.ofNullable(notification)
                                .map(NotificationType::name)
                                .orElse(null));
                        tw.columnObject(YtRow.F_CHANNEL_SERVICE, service);
                        tw.columnObject(YtRow.F_CHANNEL_EMAIL, email);
                        tw.columnObject(YtRow.F_CHANNEL_SUP, sup);

                        tw.rowEnd();
                    }
                }
            }
        };
    }

    private static class YtRow {
        static final String F_USER_ID = "user_id";
        static final String F_LOGIN = "login";
        static final String F_EMAIL = "email";
        static final String F_LANGUAGE = "language";
        static final String F_FIO = "fio";
        static final String F_HOST_ID = "host_id";
        static final String F_NOTIFICATION_TYPE = "notification_type";
        static final String F_CHANNEL_SERVICE = "channel_service";
        static final String F_CHANNEL_EMAIL = "channel_email";
        static final String F_CHANNEL_SUP = "channel_sup";
    }
}
