package ru.yandex.canvas.repository.html5;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.mongodb.client.result.UpdateResult;
import org.joda.time.DateTime;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.util.CollectionUtils;

import ru.yandex.canvas.model.html5.Batch;
import ru.yandex.canvas.model.html5.CheckStatus;
import ru.yandex.canvas.model.html5.Creative;
import ru.yandex.canvas.model.html5.Source;
import ru.yandex.canvas.repository.mongo.MongoOperationsWrapper;
import ru.yandex.canvas.repository.mongo.QueryBuilder;
import ru.yandex.canvas.service.SessionParams;
import ru.yandex.canvas.service.SessionParams.Html5Tag;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.springframework.data.mongodb.core.query.Update.update;
import static ru.yandex.canvas.repository.mongo.MongoField.ofFieldPath;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.CPM_PRICE;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.HTML5_CPM_BANNER;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.HTML5_CPM_YNDX_FRONTPAGE;
import static ru.yandex.canvas.service.SessionParams.SessionTag.CPM_BANNER;

public class BatchesRepository {

    private MongoOperationsWrapper mongoOperations;

    public BatchesRepository(MongoOperations mongoOperations) {
        this.mongoOperations = new MongoOperationsWrapper(mongoOperations, "canvas_html5_batches");
    }

    public Batch createBatch(Batch batch) {
        mongoOperations.insert(batch);
        return batch;
    }

    public List<Batch> getBatchesByIds(Long clientId, Collection<String> ids) {
        return mongoOperations.find(
                QueryBuilder.builder()
                        .and(Batch.CLIENT_ID.is(clientId))
                        .and(Batch.ARCHIVE.is(false))
                        .and(Batch.ID.in(ids))
                        .build(),
                Batch.class);
    }

    public Batch getBatchById(Long clientId, String id, SessionParams.SessionTag productType) {
        return mongoOperations.findOne(
                getProductTypeQueryBuilder(productType)
                        .and(Batch.ID.is(id))
                        .and(Batch.CLIENT_ID.is(clientId))
                        .and(Batch.ARCHIVE.is(false))
                        .build(),
                Batch.class);
    }

    public List<Batch> getBatchesByCreativeIds(Long clientId, Collection<Long> ids) {
        return mongoOperations.find(
                QueryBuilder.builder()
                        .and(Batch.CLIENT_ID.is(clientId))
                        .and(Batch.ARCHIVE.is(false))
                        .and(Batch.CREATIVE_ID.in(ids))
                        .build(),
                Batch.class);
    }

    public Batch getBatchByCreativeIdInternal(Long creativeId) {
        return mongoOperations.findOne(
                QueryBuilder.builder()
                        .and(Batch.CREATIVE_ID.is(creativeId))
                        .and(Batch.ARCHIVE.is(false))
                        .build(),
                Batch.class);
    }

    public List<Batch> getBatchesByBatchIdsIncludeArchived(List<String> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return mongoOperations.find(
                QueryBuilder.builder()
                        .and(Batch.ID.in(ids))
                        .build(),
                Batch.class);
    }

    /**
     * Аналог getBatchesByCreativeIds, но выбирающий также и архивные (удаленые) батчи, нужно крайне редко (например,
     * в превью), поэтому отдельным методом, а не параметром
     *
     * @param ids
     * @return список батчей
     */
    public List<Batch> getBatchesByCreativeIdsIncludeArchived(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return mongoOperations.find(
                QueryBuilder.builder()
                        .and(Batch.CREATIVE_ID.in(ids))
                        .build(),
                Batch.class);
    }

    /**
     * Отдает список батчей по ID клиента, имени и флагу архивности.
     * Список отдает частями по {@code limit}, для запроса новой порции нужно
     * передать {@code offset}.
     */
    public List<Batch> getBatchesByQuery(Long clientId, Integer limit, Integer offset,
                                         Sort.Direction sortOrder, Boolean archive, String name) {
        return getBatchesByQuery(clientId, limit, offset, sortOrder, archive, name, null);
    }

    /**
     * Отдает список батчей по ID клиента, имени и флагу архивности.
     * Список отдает частями по {@code limit}, для запроса новой порции нужно
     * передать {@code offset}.
     */
    public List<Batch> getBatchesByQuery(Long clientId, Integer limit, Integer offset,
                                         Sort.Direction sortOrder, Boolean archive, String name,
                                         @Nullable Html5Tag productType) {
        QueryBuilder queryBuilder = getBatchesRequestQuery(clientId, archive, name);
        addProductTypeCriteria(productType, queryBuilder);
        Query query = queryBuilder.build()
                .limit(limit)
                .skip(offset)
                .with(Sort.by(sortOrder, Batch.DATE.getFieldPath()));

        return mongoOperations.find(query, Batch.class);
    }

    /**
     * Отдает список Id батчей по полю html_replacements
     * Список отдает частями по {@code limit}, для запроса новой порции нужно
     * передать {@code offset}.
     */
    public List<Batch> getBatchesWithHtmlReplacements(DateTime date) {
        Criteria criteria = Criteria.where(Batch.CREATIVE_SOURCE_HTML_REPLACEMENTS.getFieldPath() + ".0").exists(true)
            .and(Batch.CREATIVE_SOURCE_DATE.getFieldPath()).lte(date);

        QueryBuilder queryBuilder = QueryBuilder.builder()
                .and(criteria);
        Query query = queryBuilder.build();

        return mongoOperations.find(query, Batch.class);
    }


    public List<Batch> getBatchesWithHtmlReplacementsByClientId(Long clientId, DateTime date) {
        Criteria criteria = Criteria.where(Batch.CREATIVE_SOURCE_HTML_REPLACEMENTS.getFieldPath() + ".0").exists(true)
            .and(Batch.CLIENT_ID_FIELD_NAME).is(clientId)
            .and(Batch.CREATIVE_SOURCE_DATE.getFieldPath()).lte(date);

        QueryBuilder queryBuilder = QueryBuilder.builder()
            .and(criteria);
        Query query = queryBuilder.build();

        return mongoOperations.find(query, Batch.class);
    }


    /**
     * Отдает общее количество батчей, которое можно найти по ID клиента, имени и флагу архивности.
     * Используется в связке с
     * {@link BatchesRepository#getBatchesByQuery(Long, Integer, Integer, Sort.Direction, Boolean, String)}
     */
    public long getBatchesTotalCountByQuery(Long clientId, Boolean archive, String name) {
        QueryBuilder queryBuilder = getBatchesRequestQuery(clientId, archive, name);

        return mongoOperations.count(queryBuilder.build(), Batch.class);
    }

    private QueryBuilder getBatchesRequestQuery(Long clientId, Boolean archive, String name) {
        QueryBuilder queryBuilder = QueryBuilder.builder()
                .and(Batch.CLIENT_ID.is(clientId))
                .and(Batch.ARCHIVE.is(archive));

        if (isNotBlank(name)) {
            Pattern pattern = Pattern.compile(".*" + name + ".*", Pattern.CASE_INSENSITIVE);
            queryBuilder
                    .and(Batch.NAME.regex(pattern));
        }
        return queryBuilder;
    }

    public UpdateResult updateBatchName(String id, long clientId, SessionParams.SessionTag productType, String name) {
        return mongoOperations.updateFirst(
                getProductTypeQueryBuilder(productType)
                        .and(Batch.ID.is(id))
                        .and(Batch.CLIENT_ID.is(clientId))
                        .and(Batch.ARCHIVE.is(false))
                        .build(),
                update(Batch.NAME.getFieldPath(), name),
                Batch.class);
    }

    public UpdateResult updateCreativeScreenshotUrl(Long creativeId, String screenshotUrl) {
        return mongoOperations.updateFirst(
                QueryBuilder.builder().and(Batch.CREATIVE_ID.is(creativeId)).build(),
                update(Batch.FOUND_CREATIVE_SCREENSHOT.getFieldPath(), screenshotUrl),
                Batch.class);
    }

    public UpdateResult archiveBatch(String id, long clientId, SessionParams.SessionTag productType) {
        return mongoOperations.updateFirst(
                getProductTypeQueryBuilder(productType)
                        .and(Batch.ID.is(id))
                        .and(Batch.CLIENT_ID.is(clientId))
                        .build(),
                update(Batch.ARCHIVE.getFieldPath(), true),
                Batch.class);
    }

    private QueryBuilder getProductTypeQueryBuilder(SessionParams.SessionTag productType) {
        if (productType == null || productType == CPM_BANNER) {
            return QueryBuilder.builder().and(Batch.PRODUCT_TYPE.in(CPM_BANNER, null));
        }
        if (productType == SessionParams.SessionTag.CPM_PRICE
                || productType == SessionParams.SessionTag.CPM_YNDX_FRONTPAGE) {
            return QueryBuilder.builder().and(Batch.PRODUCT_TYPE.in(SessionParams.SessionTag.CPM_YNDX_FRONTPAGE,
                    SessionParams.SessionTag.CPM_PRICE));
        }
        return QueryBuilder.builder().and(Batch.PRODUCT_TYPE.is(productType));
    }

    private void addProductTypeCriteria(@Nullable SessionParams.Html5Tag productType, QueryBuilder queryBuilder) {
        if (productType == null || productType == HTML5_CPM_BANNER) {
                queryBuilder.and(Batch.PRODUCT_TYPE.in(HTML5_CPM_BANNER.getSessionTag(), null));
                return;
        }
        if (productType == CPM_PRICE || productType == HTML5_CPM_YNDX_FRONTPAGE) {
            queryBuilder.and(Batch.PRODUCT_TYPE.in(CPM_PRICE.getSessionTag(), HTML5_CPM_YNDX_FRONTPAGE.getSessionTag()));
            if (productType == HTML5_CPM_YNDX_FRONTPAGE) {
                queryBuilder.and(Batch.FOUND_EXPANDED.exists(false));
            }
            return;
        }
        queryBuilder.and(Batch.PRODUCT_TYPE.is(productType.getSessionTag()));
    }

    public UpdateResult updateBatchHtmlReplacements(Long creativeId, List<List<String>> htmlReplacements) {
        Criteria criteria = Criteria.where(Batch.CREATIVE_ID.getFieldPath()).is(creativeId);

        return mongoOperations.updateFirst(
                new Query(criteria),
                Update.update(Batch.FOUND_CREATIVE_SOURCE_HTML_REPLACEMENTS.getFieldPath(),
                    htmlReplacements),
                Batch.class
        );
    }

    public void updateCreativeHtmlUrl(final Long creativeId, final String htmlUrl) {
        mongoOperations.updateFirst(
                QueryBuilder.builder()
                        .and(Batch.CREATIVE_ID.is(creativeId))
                        .build(),
                new Update()
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SOURCE_FIELD_NAME + "." +
                                Source.HTML_URL.getFieldPath(), htmlUrl),
                Batch.class);
    }

    public void updateCreativeValidationStatus(final Long creativeId,
                                               final CheckStatus validationStatus, final String invalidPath) {
        mongoOperations.updateFirst(
                QueryBuilder.builder()
                        .and(Batch.CREATIVE_ID.is(creativeId))
                        .build(),
                new Update()
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SOURCE_FIELD_NAME + "." +
                                ofFieldPath("validation_status").getFieldPath(), validationStatus)
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SOURCE_FIELD_NAME + "." +
                                ofFieldPath("invalid_path").getFieldPath(), invalidPath),
                Batch.class);
    }

    public UpdateResult updateScreenshotUrl(final Long creativeId, final String screenshotURL,
                                            final Boolean screenshotIsDone) {
        return mongoOperations.updateFirst(
                QueryBuilder.builder()
                        .and(Batch.CREATIVE_ID.is(creativeId))
                        .build(),
                new Update()
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SOURCE_FIELD_NAME + "." +
                                Source.SCREENSHOT_URL.getFieldPath(), screenshotURL)
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SOURCE_FIELD_NAME + "." +
                                Source.SCREENSHOT_IS_DONE.getFieldPath(), screenshotIsDone)
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SCREENSHOT_URL_FIELD_NAME, screenshotURL)
                        .set(Batch.CREATIVES_FIELD_NAME + ".$." + Creative.SCREENSHOT_IS_DONE_FIELD_NAME, screenshotIsDone),
                Batch.class);
    }

    public UpdateResult updateAdminRejectReason(Collection<Long> creativeIds, String reason) {
        Query query = QueryBuilder.builder().and(Batch.CREATIVE_ID.in(creativeIds)).build();
        Update update;
        if (reason != null) {
            update = new Update().set(Batch.CREATIVES_FIELD_NAME + ".$[c]." + Creative.ADMIN_REJECT_REASON, reason);
        } else {
            update = new Update().unset(Batch.CREATIVES_FIELD_NAME + ".$[c]." + Creative.ADMIN_REJECT_REASON);
        }
        Query filter = QueryBuilder.builder().and(Criteria.where("c.id").in(creativeIds)).build();
        return mongoOperations.updateNestedArrays(query, update, List.of(filter), Batch.class);
    }
}
