package ru.yandex.webmaster3.storage.url.checker3.dao;

import com.google.common.base.Functions;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.springframework.stereotype.Repository;
import ru.yandex.webmaster3.storage.url.checker3.data.UrlCheckDataBlock;
import ru.yandex.webmaster3.storage.url.checker3.data.UrlCheckDataBlockState;
import ru.yandex.webmaster3.storage.url.checker3.data.UrlCheckDataBlockType;
import ru.yandex.webmaster3.storage.url.common.data.UrlCheckRequestSource;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.DataMapper;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Field;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Fields;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.ValueDataMapper;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;

/**
 * @author leonidrom
 */
@Repository
public class UrlCheckDataBlocksYDao extends AbstractYDao {
    private static final String ADD_DATE_STATE_INDEX = "add_date_state_index";

    public UrlCheckDataBlocksYDao() {
        super(PREFIX_URLCHECK, "url_check_data_blocks");
    }

    public void add(UUID requestId, UrlCheckDataBlockType blockType, UrlCheckRequestSource source, DateTime addDate) {
        insert(
                F.REQUEST_ID.value(requestId),
                F.BLOCK_TYPE.value(blockType),
                F.ADD_DATE.value(addDate),
                F.SOURCE.value(source),
                F.FETCH_STATE.value(UrlCheckDataBlockState.IN_PROGRESS)
        ).execute();
    }

    public void add(UUID requestId, Collection<UrlCheckDataBlockType> blockTypes, UrlCheckRequestSource source, DateTime addDate) {
        batchInsert(getInsertDataMapper(requestId, addDate, source), blockTypes).execute();
    }

    public void delete(UUID requestId) {
        delete().where(F.REQUEST_ID.eq(requestId)).execute();
    }

    public List<UrlCheckDataBlock> get(UUID requestId) {
        return select(MAPPER)
                .where(F.REQUEST_ID.eq(requestId))
                .queryForList();
    }

    public UrlCheckDataBlock get(UUID requestId, UrlCheckDataBlockType blockType) {
        return select(MAPPER)
                .where(F.REQUEST_ID.eq(requestId)).and(F.BLOCK_TYPE.eq(blockType))
                .queryOne();
    }

    public List<UrlCheckDataBlock> getWithoutData(UUID requestId) {
        return select(MAPPER_NO_DATA)
                .where(F.REQUEST_ID.eq(requestId))
                .queryForList();
    }

    public UrlCheckDataBlock getWithoutData(UUID requestId, UrlCheckDataBlockType blockType) {
        return select(MAPPER_NO_DATA)
                .where(F.REQUEST_ID.eq(requestId)).and(F.BLOCK_TYPE.eq(blockType))
                .queryOne();
    }

    public List<UrlCheckDataBlock> getWithoutData(DateTime fromDate, DateTime toDate) {
        return select(MAPPER_NO_DATA).secondaryIndex(ADD_DATE_STATE_INDEX)
                .where(F.ADD_DATE.gte(fromDate))
                .and(F.ADD_DATE.lte(toDate))
                .queryForList();
    }

    public String getInternalError(UUID requestId) {
        return select(ERROR_MAPPER)
                .where(F.REQUEST_ID.eq(requestId))
                .queryOne();
    }

    public void closeRequest(UUID requestId, UrlCheckDataBlockType blockType, String data) {
        update(
                F.FETCH_STATE.set(UrlCheckDataBlockState.DONE),
                F.DATA.set(data.getBytes(StandardCharsets.UTF_8))
        ).where(F.REQUEST_ID.eq(requestId)).and(F.BLOCK_TYPE.eq(blockType)).execute();
    }

    public void closeRequestWithError(UUID requestId, UrlCheckDataBlockType blockType, UrlCheckDataBlockState state,
                                      String error) {
        update(
                F.FETCH_STATE.set(state),
                F.INTERNAL_ERROR.set(error)
        ).where(F.REQUEST_ID.eq(requestId)).and(F.BLOCK_TYPE.eq(blockType)).execute();
    }

    public void closeRequest(UUID requestId, UrlCheckDataBlockType blockType, UrlCheckDataBlockState fetchState) {
        update(
                F.FETCH_STATE.set(fetchState)
        ).where(F.REQUEST_ID.eq(requestId)).and(F.BLOCK_TYPE.eq(blockType)).execute();
    }

    public void forEachWithoutData(Consumer<UrlCheckDataBlock> consumer) {
        streamReader(MAPPER_NO_DATA, consumer);
    }

    private static final DataMapper<UrlCheckDataBlock> MAPPER = DataMapper.create(
            F.REQUEST_ID, F.BLOCK_TYPE, F.ADD_DATE, F.FETCH_STATE, F.DATA,
            (requestId, blockType, addDate, fetchState, data) ->
                    new UrlCheckDataBlock(requestId, blockType, addDate, fetchState,
                            data == null? null : new String(data, StandardCharsets.UTF_8)).withTimedOutCheck()
    );

    private static final DataMapper<UrlCheckDataBlock> MAPPER_NO_DATA = DataMapper.create(
            F.REQUEST_ID, F.BLOCK_TYPE, F.ADD_DATE, F.FETCH_STATE,
            (requestId, blockType, addDate, fetchState) ->
                    new UrlCheckDataBlock(requestId, blockType, addDate, fetchState, null).withTimedOutCheck()
    );

    private static final DataMapper<String> ERROR_MAPPER = DataMapper.create(
            F.INTERNAL_ERROR, s -> s
    );

    private static ValueDataMapper<UrlCheckDataBlockType> getInsertDataMapper(UUID requestId, DateTime addDate,
                                                                              UrlCheckRequestSource source) {
        return ValueDataMapper.create2(
                Pair.of(F.REQUEST_ID, t -> requestId),
                Pair.of(F.BLOCK_TYPE, t -> t),
                Pair.of(F.ADD_DATE, t -> addDate),
                Pair.of(F.SOURCE, t -> source),
                Pair.of(F.FETCH_STATE, t -> UrlCheckDataBlockState.IN_PROGRESS)
        );
    }

    private static final class F {
        static final Field<UUID> REQUEST_ID = Fields.uuidField("request_id");
        static final Field<UrlCheckDataBlockType> BLOCK_TYPE = Fields.stringEnumField("block_type", UrlCheckDataBlockType.R);
        static final Field<UrlCheckRequestSource> SOURCE = Fields.stringEnumField("source", UrlCheckRequestSource.R);
        static final Field<DateTime> ADD_DATE = Fields.jodaDateTimeField("add_date");
        static final Field<UrlCheckDataBlockState> FETCH_STATE = Fields.stringEnumField("fetch_state", UrlCheckDataBlockState.R);
        static final Field<byte[]> DATA = Fields.compressedStringField("data").makeOptional();
        static final Field<String> INTERNAL_ERROR = Fields.stringField("internal_error").makeOptional();
    }
}
