package ru.yandex.webmaster3.worker.sprav;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;

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 org.apache.commons.lang3.ObjectUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

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.util.IdUtils;
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.CommonDataType;
import ru.yandex.webmaster3.storage.settings.SettingsService;
import ru.yandex.webmaster3.storage.sprav.CompanyProfileInfo;
import ru.yandex.webmaster3.storage.sprav.CompanyProfileInfoYDao;
import ru.yandex.webmaster3.storage.sprav.FilledPercentageDetail;
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.storage.yql.YqlService;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;
import ru.yandex.wmtools.common.util.uri.UriUtils;
import ru.yandex.wmtools.common.util.uri.UriUtils.UriFeature;

/**
 * Created by ifilippov5 on 17.03.18.
 */
public class ImportCompanyProfileInfoPeriodicTask extends PeriodicTask<ImportCompanyProfileInfoPeriodicTask.TaskState> {
    private static final Logger log = LoggerFactory.getLogger(ImportCompanyProfileInfoPeriodicTask.class);

    private static final ObjectMapper OM = new ObjectMapper()
            .registerModule(new ParameterNamesModule())
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

    private static final String COMPANY_DEFAULT_NAME = "Организация";
    private static final int BATCH_SIZE = 2_000;

    private CompanyProfileInfoYDao companyProfileInfoYDao;
    private SettingsService settingsService;
    private YtService ytService;
    private YqlService yqlService;
    private YtPath ytExportPath;
    private YtPath ytPathCompanyProfile;
    private YtPath ytPathCountryPopularity;
    private boolean testMode = false;

    @Override
    public Result run(UUID runId) {
        setState(new TaskState());

        String exportQuery = testMode ? buildTestQuery() : buildQuery();
        yqlService.execute(exportQuery);

        DateTime now = DateTime.now();
        try {
            ytService.withoutTransaction(cypressService -> {
                readYtResultTable(cypressService, ytExportPath, infos -> companyProfileInfoYDao.insertBatch(infos, now));
                return true;
            });
        } catch (YtException | InterruptedException e) {
            throw new WebmasterException("Failed to read YT results table",
                    new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
        }

        settingsService.update(CommonDataType.SPRAV_COMPANY_INFO_LAST_IMPORT_DATE, String.valueOf(now));
        return new Result(TaskResult.SUCCESS);
    }

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

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

    private void readYtResultTable(YtCypressService cypressService,
                                   YtPath tablePath, Consumer<List<CompanyProfileInfo>> consumer) throws YtException {
        AsyncTableReader<YtResultRow> tableReader =
                new AsyncTableReader<>(cypressService, tablePath, Range.all(),
                        YtTableReadDriver.createYSONDriver(YtResultRow.class, OM))
                        .splitInParts(BATCH_SIZE).withThreadName("company-profile-info-cacher")
                        .withRetry(5);

        log.trace("readYtResultTable: {}", tablePath);
        List<CompanyProfileInfo> infos = new ArrayList<>();
        try (AsyncTableReader.TableIterator<YtResultRow> it = tableReader.read()) {
            while (it.hasNext()) {
                YtResultRow row = it.next();
                for (String url : row.urls) {
                    WebmasterHostId hostId;
                    try {
                        URI uri = UriUtils.toUri(url, UriFeature.CLEAN_FRAGMENT, UriFeature.DEFAULT_SCHEME_HTTP,
                                UriFeature.USE_PUNYCODED_HOSTNAME, UriFeature.NORMALIZE_PLUS_IN_QUERY).toUri();
                        String path = uri.getPath();
                        if (path != null && !path.isEmpty() && !"/".equals(path)) {
                            continue;
                        }
                        hostId = IdUtils.urlToHostId(uri.toString());
                    } catch (Exception e) {
                        log.error("Invalid url: {}", url, e);
                        continue;
                    }
                    String name = ObjectUtils.firstNonNull(row.getNameRU(), row.getNameEN(), row.getNameOTHER(),
                            row.getNameTR(), COMPANY_DEFAULT_NAME);
                    CompanyProfileInfo info = new CompanyProfileInfo(
                            hostId,
                            row.tycoonId,
                            row.permalink,
                            row.percentage,
                            row.percentageDetail,
                            row.filled,
                            row.toFill,
                            url,
                            name,
                            row.address,
                            row.popularity
                    );
                    infos.add(info);
                }

                if (infos.size() >= BATCH_SIZE) {
                    consumer.accept(infos);
                    infos.clear();
                }
            }
        } catch (IOException | InterruptedException e) {
            throw new YtException("Unable to read table: " + tablePath, e);
        }

        if (!infos.isEmpty()) {
            consumer.accept(infos);
        }
    }

    private String buildQuery() {
        return "USE " + ytExportPath.getCluster() + ";\n" +
                "INSERT INTO `" + ytExportPath.getPathWithoutCluster() + "` with truncate\n" +
                "SELECT cu.tycoon_id AS tycoon_id, cu.permalink AS permalink, cu.percentage AS percentage, cu" +
                ".percentage_detail AS percentage_detail,\n" +
                "cu.filled AS filled, cu.to_fill AS to_fill, cu.urls AS urls, c.name_ru AS name_ru, c.name_en AS " +
                "name_en, c.name_other AS name_other, c.name_tr AS name_tr, " +
                "c.address as address, c.popularity AS popularity\n" +
                "FROM `" + ytPathCompanyProfile.getPathWithoutCluster() + "` AS cu\n" +
                "JOIN `" + ytPathCountryPopularity.getPathWithoutCluster() + "` AS c\n" +
                "ON cu.permalink = c.permalink\n" +
                "WHERE\n" +
                "c.publishing_status = 'publish';";
    }

    private String buildTestQuery() {
        return "USE " + ytExportPath.getCluster() + ";\n" +
                "$testPopularity = (" + TEST_PREPARE_COMPANIES + "[" + ytPathCountryPopularity.getPathWithoutCluster() + "]);\n" +
                "INSERT INTO `" + ytExportPath.getPathWithoutCluster() + "` with truncate\n" +
                "SELECT cu.tycoon_id AS tycoon_id, cu.permalink AS permalink, cu.percentage AS percentage, cu" +
                ".percentage_detail AS percentage_detail,\n" +
                "cu.filled AS filled, cu.to_fill AS to_fill, cu.urls AS urls, c.name_ru AS name_ru, c.name_en AS " +
                "name_en, c.name_other AS name_other, c.name_tr AS name_tr, " +
                "c.address as address, c.popularity AS popularity\n" +
                "FROM `" + ytPathCompanyProfile.getPathWithoutCluster() + "` AS cu\n" +
                "JOIN $testPopularity AS c\n" +
                "ON cu.permalink = c.permalink\n" +
                "WHERE\n" +
                "c.publishing_status = 'publish';";
    }

    private static final String TEST_PREPARE_COMPANIES =
            "select permanent_id as permalink, \n" +
                    "    -- address\n" +
                    "    Yson::ConvertToString(body.address.formatted.value) as address,\n" +
                    "    Yson::ConvertToString(body.publishing_status) as publishing_status,\n" +
                    "    ListMap(ListFilter(Yson::ConvertToList(body.names), \n" +
                    "        ($name) -> { return Yson::ConvertToString($name.type) == 'main' and " +
                    "Yson::ConvertToString($name.value.locale) == 'ru';}), \n" +
                    "        ($name) -> { return Unwrap(Yson::ConvertToString($name.value.value)); }\n" +
                    "    ){0} as name_ru,\n" +
                    "    ListMap(ListFilter(Yson::ConvertToList(body.names), \n" +
                    "        ($name) -> { return Yson::ConvertToString($name.type) == 'main' and " +
                    "Yson::ConvertToString($name.value.locale) == 'en';}), \n" +
                    "        ($name) -> { return Unwrap(Yson::ConvertToString($name.value.value)); }\n" +
                    "    ){0} as name_en,\n" +
                    "    ListMap(ListFilter(Yson::ConvertToList(body.names), \n" +
                    "        ($name) -> { return Yson::ConvertToString($name.type) == 'main' and " +
                    "Yson::ConvertToString($name.value.locale) == 'tr';}), \n" +
                    "        ($name) -> { return Unwrap(Yson::ConvertToString($name.value.value)); }\n" +
                    "    ){0} as name_tr,\n" +
                    "    ListMap(ListFilter(Yson::ConvertToList(body.names), \n" +
                    "        ($name) -> { return Yson::ConvertToString($name.type) == 'main' and " +
                    "Yson::ConvertToString($name.value.locale) not in ('ru', 'en', 'tr');}), \n" +
                    "        ($name) -> { return Unwrap(Yson::ConvertToString($name.value.value)); }\n" +
                    "    ){0} as name_other,\n" +
                    "    0.0 as popularity\n" +
                    "from ";

    private static class YtResultRow {
        private final long tycoonId;
        private final long permalink;
        private final double percentage;
        private final FilledPercentageDetail percentageDetail;
        private final List<String> filled;
        private final List<String> toFill;
        private final List<String> urls;
        private final String nameRU;
        private final String nameEN;
        private final String nameOTHER;
        private final String nameTR;
        private final String address;
        private final double popularity;

        public YtResultRow(long tycoonId, long permalink, double percentage,
                           FilledPercentageDetail percentageDetail, List<String> filled, List<String> toFill,
                           List<String> urls, String nameRU, String nameEN, String nameOTHER, String nameTR,
                           String address, double popularity) {
            this.tycoonId = tycoonId;
            this.permalink = permalink;
            this.percentage = percentage;
            this.percentageDetail = percentageDetail;
            this.filled = filled;
            this.toFill = toFill;
            this.urls = urls;
            this.nameRU = nameRU;
            this.nameEN = nameEN;
            this.nameOTHER = nameOTHER;
            this.nameTR = nameTR;
            this.address = address;
            this.popularity = popularity;
        }

        @JsonProperty("tycoon_id")
        public long getTycoonId() {
            return tycoonId;
        }

        @JsonProperty("permalink")
        public long getPermalink() {
            return permalink;
        }

        @JsonProperty("percentage")
        public double getPercentage() {
            return percentage;
        }

        @JsonProperty("percentage_detail")
        public FilledPercentageDetail getPercentageDetail() {
            return percentageDetail;
        }

        @JsonProperty("filled")
        public List<String> getFilled() {
            return filled;
        }

        @JsonProperty("to_fill")
        public List<String> getToFill() {
            return toFill;
        }

        @JsonProperty("urls")
        public List<String> getUrls() {
            return urls;
        }

        @JsonProperty("name_ru")
        public String getNameRU() {
            return nameRU;
        }

        @JsonProperty("name_en")
        public String getNameEN() {
            return nameEN;
        }

        @JsonProperty("name_other")
        public String getNameOTHER() {
            return nameOTHER;
        }

        @JsonProperty("name_tr")
        public String getNameTR() {
            return nameTR;
        }

        @JsonProperty("address")
        public String getAddress() {
            return address;
        }

        @JsonProperty("popularity")
        public double getPopularity() {
            return popularity;
        }
    }

    @Required
    public void setCompanyProfileInfoYDao(CompanyProfileInfoYDao companyProfileInfoYDao) {
        this.companyProfileInfoYDao = companyProfileInfoYDao;
    }

    @Required
    public void setSettingsService(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    @Required
    public void setYtService(YtService ytService) {
        this.ytService = ytService;
    }

    @Required
    public void setYtExportPath(YtPath ytExportPath) {
        this.ytExportPath = ytExportPath;
    }

    @Required
    public void setYqlService(YqlService yqlService) {
        this.yqlService = yqlService;
    }

    @Required
    public void setYtPathCompanyProfile(YtPath ytPathCompanyProfile) {
        this.ytPathCompanyProfile = ytPathCompanyProfile;
    }

    @Required
    public void setYtPathCountryPopularity(YtPath ytPathCountryPopularity) {
        this.ytPathCountryPopularity = ytPathCountryPopularity;
    }

    @Required
    public void setTestMode(boolean testMode) {
        this.testMode = testMode;
    }

    public static class TaskState implements PeriodicTaskState {
    }
}
