package ru.yandex.webmaster3.worker.recommendedquery;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.datastax.driver.core.utils.UUIDs;
import com.google.common.base.Preconditions;
import lombok.Setter;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;
import ru.yandex.webmaster3.storage.clickhouse.ClickhouseTableInfo;
import ru.yandex.webmaster3.storage.clickhouse.TableState;
import ru.yandex.webmaster3.storage.clickhouse.TableType;
import ru.yandex.webmaster3.storage.clickhouse.dao.ClickhouseTablesRepository;
import ru.yandex.webmaster3.storage.clickhouse.replication.data.ClickhouseReplicationCommand;
import ru.yandex.webmaster3.storage.clickhouse.replication.data.ClickhouseReplicationPriority;
import ru.yandex.webmaster3.storage.recommendedquery.dao.RecommendedQueriesTables;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHTable;
import ru.yandex.webmaster3.storage.util.yt.YtException;
import ru.yandex.webmaster3.storage.util.yt.YtNode;
import ru.yandex.webmaster3.storage.util.yt.YtNodeAttributes;
import ru.yandex.webmaster3.storage.util.yt.YtOperationId;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtUtils;
import ru.yandex.webmaster3.storage.ytimport.ImportPriority;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseDataLoad;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseDataLoadState;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseDataLoadType;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseImportCommand;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseTableRelation;
import ru.yandex.webmaster3.worker.TaskSchedule;
import ru.yandex.webmaster3.worker.ytimport.AbstractYtClickhouseDataLoadTask;

/**
 * Created by ifilippov5 on 14.03.17.
 */
public final class ImportRecommendedQueriesTask extends AbstractYtClickhouseDataLoadTask {
    private static final Logger log = LoggerFactory.getLogger(ImportRecommendedQueriesTask.class);

    private static final String ATTR_LAST_PROCESSED = "modification_time";
    private static final int PREPARED_TABLE_LINES_COUNT = 1024;
    private static final DateTimeFormatter modificationDateTimeFormat = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");

    @Setter
    private boolean isExtended;
    @Setter
    private CHTable resultingTable;
    @Setter
    private RecommendedQueriesTables recommendedQueriesTables;
    @Setter
    private ClickhouseTablesRepository clickhouseTablesCDao;
    @Setter
    private YtPath ytWorkDir;
    @Setter
    private YtPath ytPrepareExecutable;
    @Setter
    private PeriodicTaskType type;

    public YtClickhouseDataLoadType getImportType() {
        return isExtended ? YtClickhouseDataLoadType.EXTENDED_RECOMMENDED_QUERIES
                          : YtClickhouseDataLoadType.RECOMMENDED_QUERIES;
    }

    @Override
    protected YtClickhouseDataLoad init(YtClickhouseDataLoad latestImport) throws Exception {
        Instant updateDate = getDataUpdateTimeForTableOnYT();
        if (updateDate != null) {
            if (latestImport.getData() == null || updateDate.getMillis() > Long.parseLong(latestImport.getData())) {
                LocalDate date = updateDate.toDateTime().toLocalDate(); // only for table
                return latestImport.withSourceTable(tablePath, date, date)
                        .withData(String.valueOf(updateDate.getMillis()));
            }
        }
        return latestImport.withState(YtClickhouseDataLoadState.DONE); // nothing to do here
    }

    @Override
    protected YtClickhouseDataLoad prepare(YtClickhouseDataLoad importData) {
        int shardsCount = clickhouseServer.getShardsCount();
        List<YtPath> outTables = new ArrayList<>(shardsCount);
        String preparedTableName = importData.getData() + "_";
        for (int shard = 0; shard < shardsCount; shard++) {
            outTables.add(YtPath.path(ytWorkDir, preparedTableName + shard));
        }
        try {
            ytService.inTransaction(ytWorkDir).execute(cypressService -> {
                YtUtils.recreateTables(cypressService, outTables, new YtNodeAttributes().setCompressionCodec("none"));
                YtOperationId operationId = cypressService.mapReduce(
                        YtUtils.newPrepareTablesForImportBuilder()
                                .addInputTable(importData.getSourceTable())
                                .setOutputTables(outTables)
                                .setBinary(ytPrepareExecutable)
                                .setTask(isExtended ? "EXTENDED_RECOMMENDED_QUERIES" : "RECOMMENDED_QUERIES")
                                .setLines(PREPARED_TABLE_LINES_COUNT)
                                .build());

                if (!cypressService.waitFor(operationId)) {
                    throw new YtException("Prepare favorite search queries failed. See " + operationId);
                }
                return true;
            });
        } catch (YtException e) {
            throw new RuntimeException(e);
        }
        return importData.withPreparedTables(outTables).withNextState();
    }

    @Override
    protected YtClickhouseDataLoad doImport(YtClickhouseDataLoad importData) {
        UUID taskId = UUIDs.timeBased();
        List<YtClickhouseTableRelation> tablesRels = new ArrayList<>();
        List<YtPath> preparedTables = importData.getPreparedTables();
        for (int shard = 0; shard < preparedTables.size(); shard++) {
            tablesRels.add(new YtClickhouseTableRelation(
                    preparedTables.get(shard),
                    shard,
                    resultingTable.replicatedMergeTreeTableName(-1, importData.getData()),
                    resultingTable.createReplicatedMergeTreeSpec(-1, importData.getData())
            ));
        }
        YtClickhouseImportCommand importCommand = new YtClickhouseImportCommand(
                taskId,
                tablesRels,
                resultingTable.getDatabase(),
                resultingTable.importSpec(),
                ImportPriority.ONLINE
        );
        ytClickhouseImportManager.startImport(importCommand);
        return importData.withImportTaskIds(taskId).withNextState();
    }

    @Override
    protected YtClickhouseDataLoad replicate(YtClickhouseDataLoad importData) {
        int shards = clickhouseServer.getShardsCount();
        List<ClickhouseReplicationCommand.TableInfo> tables = new ArrayList<>();
        for (int shard = 0; shard < shards; shard++) {
            tables.add(new ClickhouseReplicationCommand.TableInfo(
                    resultingTable.replicatedMergeTreeTableName(-1, importData.getData()),
                    resultingTable.createReplicatedMergeTreeSpec(-1, importData.getData()),
                    shard
            ));
        }
        UUID taskId = UUIDs.timeBased();
        ClickhouseReplicationCommand replicationCommand = new ClickhouseReplicationCommand(
                taskId,
                resultingTable.getDatabase(),
                ClickhouseReplicationPriority.ONLINE,
                tables
        );
        clickhouseReplicationManager.enqueueReplication(replicationCommand);
        try {
            // добавим запись о новых таблицах в Cassandra
            clickhouseTablesCDao.update(recommendedQueriesTables.toClickhouseTable(resultingTableType(), resultingTable, importData));
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Error writing table info to Cassandra",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        }
        return importData.withReplicationTaskIds(taskId).withNextState();
    }

    private TableType resultingTableType() {
        return isExtended ? TableType.EXTENDED_RECOMMENDED_QUERIES : TableType.RECOMMENDED_QUERIES;
    }

    @Override
    protected YtClickhouseDataLoad rename(YtClickhouseDataLoad imprt) throws Exception {
        // возьмем последние версии таблиц
        Map<TableType, ClickhouseTableInfo> lastTables = clickhouseTablesCDao.listTables().stream()
                .filter(table -> table.getType() == TableType.RECOMMENDED_QUERIES ||
                        table.getType() == TableType.EXTENDED_RECOMMENDED_QUERIES)
                .collect(Collectors.toMap(ClickhouseTableInfo::getType, Function.<ClickhouseTableInfo>identity(),
                        BinaryOperator.maxBy(Comparator.naturalOrder())));
        Preconditions.checkState(lastTables.size() == 2);
        lastTables.forEach((type, table) -> {
            if (table.getType() == resultingTableType() && table.getState() == TableState.DEPLOYED) {
                // сразу включим нашу таблицу
                clickhouseTablesCDao.update(table.withState(TableState.ON_LINE));
            }
        });
        recommendedQueriesTables.updateMergeTable(lastTables.get(TableType.RECOMMENDED_QUERIES),
                lastTables.get(TableType.EXTENDED_RECOMMENDED_QUERIES));
        return imprt.withNextState();
    }

    private Instant getDataUpdateTimeForTableOnYT() throws YtException, InterruptedException {
        return ytService.withoutTransactionQuery(cypressService -> {
            if (!cypressService.exists(tablePath)) {
                return null;
            }
            YtNode result = cypressService.getNode(tablePath);
            String modificationTimeAsText = result.getNodeMeta().get(ATTR_LAST_PROCESSED).asText();
            DateTime dateTime = DateTime.parse(modificationTimeAsText, modificationDateTimeFormat);

            log.debug("Modification datetime: {}", dateTime);

            return new Instant(dateTime);
        });
    }

    @Override
    protected YtClickhouseDataLoad createDistributedTables(YtClickhouseDataLoad imprt) throws Exception {
        resultingTable.updateDistributedSymlink(clickhouseServer, imprt.getData());
        return imprt.withNextState();
    }

    @Override
    public PeriodicTaskType getType() {
        return type;
    }

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

    public void setResultingTableName(String resultingTableName) {
        this.resultingTable = RecommendedQueriesTables.recommendedQueriesTableForName(resultingTableName);
    }
}
