package ru.yandex.webmaster3.worker.turbo;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Range;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Value;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemTypeEnum;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.turbo.model.TurboHostSettings;
import ru.yandex.webmaster3.core.turbo.model.commerce.TurboCommerceSettings;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentProvider;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentType;
import ru.yandex.webmaster3.storage.checklist.data.ProblemSignal;
import ru.yandex.webmaster3.storage.checklist.service.SiteProblemsNotificationService;
import ru.yandex.webmaster3.storage.clickhouse.ClickhouseTableInfo;
import ru.yandex.webmaster3.storage.clickhouse.TableSource;
import ru.yandex.webmaster3.storage.clickhouse.TableState;
import ru.yandex.webmaster3.storage.clickhouse.TableType;
import ru.yandex.webmaster3.storage.events.data.events.RetranslateToUsersEvent;
import ru.yandex.webmaster3.storage.events.data.events.UserDomainMessageEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.settings.SettingsService;
import ru.yandex.webmaster3.storage.turbo.dao.TurboDomainsStateHelper;
import ru.yandex.webmaster3.storage.turbo.service.TurboDomainsStateService;
import ru.yandex.webmaster3.storage.turbo.service.TurboFeedsService;
import ru.yandex.webmaster3.storage.turbo.service.settings.TurboSettingsService;
import ru.yandex.webmaster3.storage.user.message.MessageTypeEnum;
import ru.yandex.webmaster3.storage.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHField;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHTable;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
import ru.yandex.webmaster3.storage.util.yt.YtCypressService;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtTableReadDriver;
import ru.yandex.webmaster3.storage.yql.YqlFunctions;
import ru.yandex.webmaster3.storage.yql.YqlQueryBuilder;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseDataLoad;
import ru.yandex.webmaster3.storage.ytimport.YtClickhouseDataLoadType;
import ru.yandex.webmaster3.worker.TaskSchedule;

import static ru.yandex.webmaster3.storage.host.CommonDataType.LAST_IMPORTED_TURBO_DOMAINS_STATE_CHANGES;
import static ru.yandex.webmaster3.storage.turbo.dao.TurboDomainsStateHelper.F;

/**
 * Created by Oleg Bazdyrev on 03/08/2020.
 */
@Slf4j
public class ImportTurboDomainsStateTask extends AbstractYqlPrepareImportTask {

    private static final int LINES_COUNT = 1024;
    private static final Duration PROCESSED_MAX_DATA_AGE = Duration.standardDays(5L);

    private static final Map<String, String> SOURCE_EXPRESSIONS = new ImmutableMap.Builder<String, String>()
            .put(F.DATE, "'0000-00-00'")
            .put(F.DOMAIN, "domain")
            .put(F.RSS_FEEDS, "String::ReplaceAll(cast(Yson::SerializeJson(rss_feeds) as String), @@\\@@, @@\\\\@@)")
            .put(F.YML_FEEDS, "String::ReplaceAll(cast(Yson::SerializeJson(yml_feeds) as String), @@\\@@, @@\\\\@@)")
            .put(F.BANS, "String::ReplaceAll(cast(Yson::SerializeJson(bans) as String), @@\\@@, @@\\\\@@)")
            .put(F.AUTORELATED_SAMPLES, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(autorelated_samples) as String), '[]'), @@\\@@, @@\\\\@@)")
            .put(F.AUTOMORDA_SAMPLES, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(automorda_samples) as String), '[]'), @@\\@@, @@\\\\@@)")
            .put(F.AUTOMORDA_STATUS, "nvl(automorda_status, '')")
            .put(F.APP_REVIEWS_INFO, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(app_reviws_info) as String), 'null'), @@\\@@, @@\\\\@@)")
            .put(F.AUTOPARSER_SAMPLES, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(autoparser_samples) as String), '[]'), @@\\@@, @@\\\\@@)")
            .put(F.COMMERCE_CATEGORIES, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(commerce_categories) as String), '[]'), @@\\@@, @@\\\\@@)")
            .put(F.EXPERIMENT, "String::EscapeC(nvl(experiment, ''))")
            .put(F.LISTINGS_INFO, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(listings_info) as String), 'null'), @@\\@@, @@\\\\@@)")
            .put(F.MARKET_FEEDS, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(market_feeds) as String), '[]'), @@\\@@, @@\\\\@@)")
            .put(F.PROBLEMS, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(problems) as String), '{}'), @@\\@@, @@\\\\@@)")
            .put(F.PREMODERATION_RESULT, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(premoderation_result) as String), '{}'), @@\\@@, @@\\\\@@)")
            .put(F.BANNED_SCC, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(banned_scc) as String), '{}'), @@\\@@, @@\\\\@@)")
            .put(F.SHOP_STATES, "String::ReplaceAll(nvl(cast(Yson::SerializeJson(shop_states) as String), '{}'), @@\\@@, @@\\\\@@)")
            .build();

    @Setter
    private SettingsService settingsService;
    @Setter
    private TurboDomainsStateService turboDomainsStateService;
    @Setter
    private TurboSettingsService turboSettingsService;
    @Setter
    private TurboFeedsService turboFeedsService;
    @Setter
    private boolean sendNotifications = true;
    @Setter
    private SiteProblemsNotificationService siteProblemsNotificationService;
    @Setter
    private WMCEventsService wmcEventsService;

    @Value("${webmaster3.worker.turbo.domainsState.import.changesDir}")
    private YtPath changesDir;

    @Override
    protected CHTable getTable() {
        return TurboDomainsStateHelper.TABLE;
    }

    @Override
    protected TableType getTableType() {
        return TableType.TURBO_DOMAINS_STATE;
    }

    @Override
    protected int getShardsCount() {
        return 1; // no sharding
    }

    @Override
    protected YqlQueryBuilder prepareIntermediateTable(YtClickhouseDataLoad imprt) {
        String fields = getTable().getFields().stream().map(CHField::getName).map(SOURCE_EXPRESSIONS::get)
                .collect(Collectors.joining(" || '\\t' || ", "(", " || '\\n')"));

        YqlQueryBuilder yqlQueryBuilder = YqlQueryBuilder.newBuilder()
                .cluster(tablePath)
                .appendFDefinition(YqlFunctions.URL_2_HOST_ID)
                .appendText("PRAGMA yt.MaxRowWeight = '128M';\n")
                .appendText("INSERT INTO " + INTERMEDIATE_TABLE)
                .appendText("SELECT ShardId, RowId, Compress::Gzip(String::JoinFromList(AGGREGATE_LIST(data), ''), 6) as data FROM (\n")
                .appendText("SELECT (Digest::Fnv64(domain) % " + getShardsCount() + ") as ShardId,")
                .appendText("((Digest::Fnv64(domain) / " + getShardsCount() + ") % " + LINES_COUNT + ") as RowId,")
                .appendText(fields).appendText("as data ")
                .appendText("FROM")
                .appendTable(imprt.getSourceTable())
                .appendText(") \n GROUP BY ShardId, RowId;")
                .appendText("COMMIT;\n\n");

        return yqlQueryBuilder;
    }

    @Override
    protected YtClickhouseDataLoad rename(YtClickhouseDataLoad imprt) throws Exception {
        // включаем таблицу, чтобы прорастала.
        changeTableToOnline(imprt);
        if (!sendNotifications) {
            return imprt.withNextState();
        }
        long imprtTimestamp = Long.parseLong(imprt.getData());
        // импорт изменений и рассылка оповещений
        // у нас есть защита от повторных отправок, так что не будем заморачиваться с ретраями
        ytService.withoutTransaction(cypressService -> {
            long lastProcessedTimestamp = Optional.ofNullable(settingsService.getSettingUncached(LAST_IMPORTED_TURBO_DOMAINS_STATE_CHANGES))
                    .map(CommonDataState::getValue).map(Long::valueOf).orElse(0L);
            List<YtPath> tablesToProcess = cypressService.list(changesDir);
            tablesToProcess.sort(null);
            log.info("Found {} changes tables to process: {}", tablesToProcess.size(), tablesToProcess);
            for (YtPath table : tablesToProcess) {
                long tableTimestamp = Long.parseLong(table.getName());
                if (tableTimestamp > imprtTimestamp) {
                    break;
                }
                if (tableTimestamp <= lastProcessedTimestamp) {
                    continue;
                }
                // process notifications
                processChanges(cypressService, table);

                lastProcessedTimestamp = tableTimestamp;
                settingsService.update(LAST_IMPORTED_TURBO_DOMAINS_STATE_CHANGES, String.valueOf(lastProcessedTimestamp));
            }

            if (YandexEnvironmentProvider.getEnvironmentType() == YandexEnvironmentType.PRODUCTION) {
                cleanup(cypressService);
            }

            return true;
        });

        return imprt.withNextState();
    }

    private void changeTableToOnline(YtClickhouseDataLoad imprt) {
        String chTableName = getTable().getDatabase() + "." + getTable().replicatedMergeTreeTableName(0, imprt.getData());
        final List<ClickhouseTableInfo> clickhouseTableInfos = clickhouseTablesCDao.listTables(getTableType());
        final boolean tableAlreadyInserted = clickhouseTableInfos.stream()
                .anyMatch(x -> x.getClickhouseFullName().equals(chTableName));

        if (!tableAlreadyInserted) {
            clickhouseTablesCDao.update(
                    new ClickhouseTableInfo(getTableType(), UUIDs.timeBased(),
                            TableState.ON_LINE, DateTime.now(), TableSource.YT_HAHN, null,
                            chTableName, chTableName, chTableName, imprt.getPreparedTables().size(), getTable().getParts())
            );
        }
    }

    private void processChanges(YtCypressService cypressService, YtPath table) throws InterruptedException {
        log.info("Processing changes table {}", table);
        DateTime now = DateTime.now();
        AsyncTableReader<ChangesRecord> tableReader = new AsyncTableReader<>(cypressService, table, Range.all(),
                YtTableReadDriver.createYSONDriver(ChangesRecord.class)).withThreadName("turbo-domains-state-changes-reader");
        try (var iterator = tableReader.read()) {
            while (iterator.hasNext()) {
                ChangesRecord record = iterator.next();
                WebmasterHostId hostId;
                String domain = record.getDomain();
                try {
                    hostId = IdUtils.urlToHostId(domain);
                } catch (IllegalArgumentException e) {
                    log.warn("Bad domain: ", domain);
                    continue;
                }
                SiteProblemTypeEnum problemType = EnumUtils.getEnum(SiteProblemTypeEnum.class, record.getType());
                MessageTypeEnum messageType = EnumUtils.getEnum(MessageTypeEnum.class, record.getType());
                // alerts
                if (problemType != null) {
                    DateTime lastUpdate = record.lastUpdate == null ? DateTime.now() : new DateTime(record.lastUpdate);
                    DateTime actualSince = record.actualSince == null ? DateTime.now() : new DateTime(record.actualSince);
                    ProblemSignal problemSignal = new ProblemSignal(
                            turboDomainsStateService.createProblemContentFromRawData(problemType, record.getData()), lastUpdate);

                    siteProblemsNotificationService.sendNotification(hostId, problemSignal, null, actualSince);
                }
                if (messageType != null) {
                    UserDomainMessageEvent event = null;
                    switch (messageType) {
                        case TURBO_AUTOPARSED_PAGES_APPEARED:
                            event = UserDomainMessageEvent.create(hostId, new MessageContent.TurboAutoparsedPagesAppeared(hostId),
                                    NotificationType.TURBO_NEW, false);
                            break;
                        case TURBO_LISTINGS_AVAILABLE: {
                            event = UserDomainMessageEvent.create(hostId, new MessageContent.TurboListingsAvailable(hostId),
                                    NotificationType.TURBO_LISTINGS_NEW, false);
                            // включим листинги
                            TurboHostSettings settings = turboSettingsService.getSettings(domain);
                            TurboHostSettings.TurboHostSettingsBuilder builder = new TurboHostSettings.TurboHostSettingsBuilder(settings);
                            TurboCommerceSettings.TurboCommerceSettingsBuilder csb = new TurboCommerceSettings.TurboCommerceSettingsBuilder(
                                    settings.getCommerceSettings());
                            csb.setTurboListingsEnabled(true);
                            builder.setCommerceSettings(csb.build());
                            turboSettingsService.updateSettings(domain, builder.build());
                            break;
                        }

                        case TURBO_SCC_BANNED:
//                            event = UserDomainMessageEvent.create(hostId,
//                                    new MessageContent.TurboSccBanned(hostId), NotificationType.TURBO_SCC_BANNED, true);
//                            turboFeedsService.disableYmlFeeds(domain);
//                            turboSettingsService.notifyAboutSettingsChange(domain, null, null, null);
                            break;
                        case TURBO_SCC_UNBANNED:
//                            event = UserDomainMessageEvent.create(hostId,
//                                    new MessageContent.TurboSccUnbanned(hostId), NotificationType.TURBO_SCC_UNBANNED, false);
                            break;
                        case TURBO_SCC_PASS:
//                            event = UserDomainMessageEvent.create(hostId,
//                                    new MessageContent.TurboSccPass(hostId), NotificationType.TURBO_SCC_PASS, false);
                            break;
                        case TURBO_SCC_FAILED:
//                            event = UserDomainMessageEvent.create(hostId,
//                                    new MessageContent.TurboSccFailed(hostId), NotificationType.TURBO_SCC_FAILED, true);
//                            turboFeedsService.disableYmlFeeds(domain);
//                            turboSettingsService.notifyAboutSettingsChange(domain, null, null, null);
                            break;
                        default:
                            throw new IllegalStateException("Unsupported message type: " + problemType);
                    }
                    if (event != null) {
                        log.info("About to send event {} to host {}", event.getMessageContent().getType(), hostId);
                        wmcEventsService.addEvent(new RetranslateToUsersEvent<>(event));
                    }
                }
            }
        } catch (IOException e) {
            throw new WebmasterException("Error reading table", new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
        }
    }

    private void cleanup(YtCypressService cypressService) {
        long maxTimestamp = DateTime.now().minus(PROCESSED_MAX_DATA_AGE).getMillis();
        for (YtPath table : cypressService.list(changesDir)) {
            long tableTimestamp = Long.parseLong(table.getName());
            if (tableTimestamp < maxTimestamp) {
                log.info("Removing old table {}", table);
                cypressService.remove(table);
            }
        }
    }

    @Override
    protected YtClickhouseDataLoadType getImportType() {
        return YtClickhouseDataLoadType.TURBO_DOMAINS_STATE;
    }

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

    @lombok.Value
    private static final class ChangesRecord {
        String domain;
        String type;
        JsonNode data;
        @JsonProperty("last_update")
        Long lastUpdate;
        @JsonProperty("actual_since")
        Long actualSince;
    }
}
