package ru.yandex.webmaster3.worker.metrika;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import com.google.common.collect.Range;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.mutable.MutableInt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersUtil;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;
import ru.yandex.webmaster3.storage.user.service.UserHostsService;
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.YtMissingValueMode;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtRowMapper;
import ru.yandex.webmaster3.storage.util.yt.YtService;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

/**
 * Created by ifilippov5 on 06.12.17.
 */
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ImportDomainsWithPvlPeriodicTask extends PeriodicTask<PeriodicTaskState> {

    private final UserHostsService userHostsService;
    private final YtService ytService;
    @Setter
    private YtPath ytPath;
    @Setter
    private Resource pvlUsersDefault;

    @Override
    public Result run(UUID runId) {
        Set<Long> userIds = new HashSet<>();
        addDefaultUsersWithPvl(userIds::add);
        processYtResults(rows -> {
            rows.stream()
                    .flatMap(row -> MetrikaCountersUtil.generateHostIds(MetrikaCountersUtil.domainToCanonicalAscii(row.getDomain())))
                    .forEach(hostId -> {
                        try {
                            userIds.addAll(userHostsService.listUsersVerifiedHost(hostId).keySet());
                        } catch (WebmasterYdbException e) {
                            throw new WebmasterException("Failed to get users verified host",
                                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
                        }
                    });
        });
        return new Result(TaskResult.SUCCESS);
    }

    private void addDefaultUsersWithPvl(Consumer<Long> consumer) {
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(pvlUsersDefault.getInputStream(), StandardCharsets.UTF_8))) {
            String userIdStr;
            while ((userIdStr = br.readLine()) != null) {
                long userId = Long.parseLong(userIdStr);
                consumer.accept(userId);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

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

    public void processYtResults(Consumer<List<YtResultRow>> rowConsumer) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        try {
            processYtResultTable(executorService, ytPath, rowConsumer);
        } catch (WebmasterException e) {
            throw new RuntimeException(e);
        }

        executorService.shutdown();
        try {
            executorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.warn("Unable to terminate executor", e);
        }
    }

    private void processYtResultTable(ExecutorService executorService, YtPath tablePath, Consumer<List<YtResultRow>> rowConsumer) {
        try {
            ytService.withoutTransaction(cypressService -> {
                readYtResultTable(executorService, cypressService, tablePath, rowConsumer);
                return true;
            });
        } catch (YtException | InterruptedException e) {
            throw new WebmasterException("Failed to read YT results table",
                    new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
        }
    }

    private void readYtResultTable(ExecutorService executorService, YtCypressService cypressService,
                                   YtPath tablePath, Consumer<List<YtResultRow>> rowConsumer) throws YtException {
        AsyncTableReader<YtResultRow> tableReader =
                new AsyncTableReader<>(cypressService, tablePath, Range.all(), new YtResultRowMapper(),
                        YtMissingValueMode.PRINT_SENTINEL)
                        .splitInParts(200_000)
                        .inExecutor(executorService, "pvl-domains-cacher")
                        .withRetry(5);

        log.trace("readYtResultTable: {}", tablePath);
        MutableInt count = new MutableInt(0);
        List<YtResultRow> rows = new ArrayList<>();
        try (AsyncTableReader.TableIterator<YtResultRow> it = tableReader.read()) {
            while (it.hasNext()) {
                YtResultRow row = it.next();
                rows.add(row);
                count.increment();
                if (rows.size() == 200_000) {
                    log.info("{} rows read from yt", count.getValue());
                    rowConsumer.accept(rows);
                    rows.clear();
                }
            }
        } catch (IOException | InterruptedException e) {
            throw new YtException("Unable to read table: " + tablePath, e);
        }
        if (rows.size() > 0) {
            rowConsumer.accept(rows);
        }
    }

    private static class YtResultRow {
        static final String DOMAIN = "domain";

        Map<String, String> fields = new HashMap<>();

        String getDomain() {
            return getString(DOMAIN);
        }

        private String getString(String field) {
            return fields.get(field);
        }
    }

    private static class YtResultRowMapper implements YtRowMapper<YtResultRow> {
        private YtResultRow row;

        YtResultRowMapper() {
            this.row = new YtResultRow();
        }

        @Override
        public void nextField(String name, InputStream data) {
            try {
                String fieldStr = IOUtils.toString(data, StandardCharsets.UTF_8);
                row.fields.put(name, fieldStr);
            } catch (Exception e) {
                log.error("Unable to read Yt result field: {}", name, e);
            }
        }

        @Override
        public YtResultRow rowEnd() {
            YtResultRow r = row;
            log.trace("{}", row.fields.toString());
            row = new YtResultRow();

            return r;
        }

        @Override
        public List<String> getColumns() {
            return Arrays.asList(YtResultRow.DOMAIN);
        }
    }

}
