package ru.yandex.webmaster3.worker.turbo.api;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.google.common.collect.Range;
import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.Value;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.host.service.HostOwnerService;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboApiTaskWithResult;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboCrawlState;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedItemStatistics;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.RetryUtils;
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.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.SettingsService;
import ru.yandex.webmaster3.storage.turbo.dao.api.TurboApiHostTasksYDao;
import ru.yandex.webmaster3.storage.turbo.service.TurboApiPagesRawStatsData;
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;

/**
 * Created by ifilippov5 on 21.05.18.
 */
public class ImportTurboApiResultsTask extends PeriodicTask<PeriodicTaskState> {

    private static final Logger log = LoggerFactory.getLogger(ImportTurboApiResultsTask.class);
    private static final long TABLE_AGE_FOR_REMOVE = Duration.standardDays(1L).getMillis();
    private static final RetryUtils.RetryPolicy CS_WRITE_RETRY_POLICY = RetryUtils.linearBackoff(5, Duration.standardSeconds(30));

    private static final ObjectMapper OM = new ObjectMapper()
            .registerModule(new ParameterNamesModule())
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("\\d+(\\.debug)?");

    @Setter
    private String previewServiceUrl;
    @Setter
    private HostOwnerService hostOwnerService;
    @Setter
    private SettingsService settingsService;
    @Setter
    private TurboApiHostTasksYDao turboApiHostTasksYDao;
    @Setter
    private YtService ytService;
    @Setter
    private List<YtPath> rootPaths;
    @Setter
    private List<YtPath> rootDebugPaths;

    @Override
    public Result run(UUID runId) throws Exception {
        TaskResult result = TaskResult.SUCCESS;
        for (YtPath rootPath : rootPaths) {
            try {
                ytService.inTransaction(rootPath).query(cypressService -> checkDataForTable(cypressService, rootPath, true));
            } catch (Exception e) {
                log.error("Error when importing turbo api results from " + rootPath, e);
                result = TaskResult.FAIL;
            }
        }
        for (YtPath rootPath : rootDebugPaths) {
            try {
                ytService.inTransaction(rootPath).query(cypressService -> checkDataForTable(cypressService, rootPath, false));
            } catch (Exception e) {
                log.error("Error when importing turbo api results from " + rootPath, e);
                result = TaskResult.FAIL;
            }
        }
        return new Result(result);
    }

    private boolean checkDataForTable(YtCypressService cypressService, YtPath rootPath, boolean production) throws YtException {
        CommonDataType key = production ? CommonDataType.TURBO_FEEDS_API_RESULTS_LAST_IMPORT : CommonDataType.TURBO_FEEDS_API_DEBUG_RESULTS_LAST_IMPORT;
        CommonDataState state = settingsService.getSettingUncached(key);
        String lastImportTableName = (state == null ? null : state.getValue());
        long lastImportTableDate = lastImportTableName == null ? 0L : Long.parseLong(lastImportTableName);
        List<YtPath> importTables = new ArrayList<>();

        List<YtPath> tables = cypressService.list(rootPath);
        tables.sort(YtPath::compareTo);
        for (YtPath table : tables) {
            if (!TABLE_NAME_PATTERN.matcher(table.getName()).matches()) {
                continue;
            }
            long tableDate = cypressService.getNode(table).getUpdateTime().getMillis();
            if (tableDate > lastImportTableDate) {
                // нашли свежую таблицу
                importTables.add(table);
            } else if ((lastImportTableDate - tableDate) > TABLE_AGE_FOR_REMOVE) {
                // удалим ненужную таблицу
                cypressService.remove(table);
            }
        }

        if (importTables.isEmpty()) {
            log.info("No fresh data on YT");
            return false;
        }

        log.info("Tables count to process: {}", importTables.size());
        // если в yt есть свежие данные, нужно синхронизировать их с кассандрой
        log.info("Start sync turbo statistics with Cassandra table");

        TaskState ts = new TaskState();
        ts.newTablesCount = importTables.size();
        setState(ts);

        for (YtPath importTable : importTables) {
            log.info("Processing table {}", importTable);
            ExecutorService executorService = Executors.newFixedThreadPool(1);
            try {
                readYtTable(executorService, cypressService, importTable, Range.all(), DateTime.now(), production);
                long tableDate = cypressService.getNode(importTable).getUpdateTime().getMillis();
                settingsService.update(key, String.valueOf(tableDate));
            } finally {
                executorService.shutdown();
            }
        }
        log.info("Finish sync turbo statistics with Cassandra table");

        return true;
    }

    /**
     * Вычитывает данные из Yt таблички и параллельно пишет в кассандру
     */
    private void readYtTable(ExecutorService executorService, YtCypressService cypressService, YtPath tablePath, Range<Long> range,
                             DateTime importDate, boolean production) throws YtException {
        AsyncTableReader<TurboLogRow> tableReader = new AsyncTableReader<>(cypressService, tablePath, range,
                YtTableReadDriver.createYSONDriver(TurboLogRow.class, OM)).splitInParts(10000L)
                .inExecutor(executorService, "turbo-feeds-api-crawl-statistics-cacher")
                .withRetry(5);

        log.trace("readYtResultTable: {}", tablePath);
        try (AsyncTableReader.TableIterator<TurboLogRow> it = tableReader.read()) {
            while (it.hasNext()) {
                TurboLogRow row = it.next();
                processFeed(row, importDate, production);
            }
        } catch (Exception e) {
            throw new YtException("Unable to read table: " + tablePath, e);
        }
    }

    private void processFeed(TurboLogRow row, DateTime importDate, boolean production) throws Exception {
        UUID taskId;
        try {
            taskId = UUID.fromString(row.feed);
        } catch (Exception e) {
            log.error("Feed not UUID type!");
            return;
        }
        WebmasterHostId hostId = IdUtils.urlToHostId(row.host);
        if (Optional.ofNullable(turboApiHostTasksYDao.getTask(hostId, taskId))
                .map(TurboApiTaskWithResult::getState).orElse(TurboCrawlState.OK) != TurboCrawlState.PROCESSING) {
            log.debug("TaskId {} lost or already imported", taskId);
            return;
        }

        turboApiHostTasksYDao.add(makeTaskWithResult(hostId, taskId, row.data, importDate, production));
    }

    private TurboApiTaskWithResult makeTaskWithResult(WebmasterHostId hostId, UUID taskId, TurboApiPagesRawStatsData data, DateTime importDate,
                                                      boolean production) {
        TurboFeedItemStatistics stats = TurboFeedItemStatistics.fromObjectNode(data.getStats());
        if (stats == null) {
            stats = TurboFeedItemStatistics.EMPTY;
        }
        // нет ни одного турбо-урла - ошибка
        TurboCrawlState state;
        if (!data.hasUrls() && stats.getValid() == 0) {
            state = TurboCrawlState.ERROR;
        } else if (data.hasErrors()) {
            state = TurboCrawlState.WARNING;
        } else {
            state = TurboCrawlState.OK;
        }
        String owner = hostOwnerService.getHostOwner(IdUtils.hostIdToUrl(hostId));
        return TurboApiTaskWithResult.builder()
                .owner(owner)
                .hostId(hostId)
                .addDate(new DateTime(UUIDs.unixTimestamp(taskId)))
                .taskId(taskId)
                .active(production)
                .importDate(importDate)
                .state(state)
                .stats(stats)
                .hash(data.getHash())
                .urls(data.collectTurboUrls(previewServiceUrl, true))
                .errors(data.collectTurboRawErrors())
                .build();
    }

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

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

    @Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    public static class TurboLogRow {
        String feed;
        String host;
        long timestamp;
        @JsonProperty("last_access")
        long lastAccess;
        TurboApiPagesRawStatsData data;
    }

    class TaskState implements PeriodicTaskState {
        public int newTablesCount;

        public int getNewTablesCount() {
            return newTablesCount;
        }
    }

}
