package ru.yandex.canvas.model.validation;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.annotation.Nonnull;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.google.common.base.Preconditions;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

import ru.yandex.canvas.model.CreativeData;
import ru.yandex.canvas.model.CreativeDocument;
import ru.yandex.canvas.model.elements.Element;
import ru.yandex.canvas.model.presets.Preset;
import ru.yandex.canvas.model.presets.PresetItem;
import ru.yandex.canvas.model.validation.presetbased.creative.CreativeDataValidator;
import ru.yandex.canvas.model.validation.presetbased.elements.ElementValidator;
import ru.yandex.canvas.service.AvatarsService;
import ru.yandex.canvas.service.FileService;
import ru.yandex.canvas.service.PresetsService;
import ru.yandex.canvas.service.video.InBannerVideoFilesService;

import static java.util.Collections.emptyList;
import static java.util.function.Function.identity;
import static ru.yandex.direct.utils.CommonUtils.nvl;

/**
 * Валидатор для {@link CreativeDocument} и его дочерних полей на основе PresetId
 */
public class CreativeDocumentPresetBasedValidator
        implements ConstraintValidator<PresetBasedValidation, CreativeDocument> {
    private static final Logger logger = LoggerFactory.getLogger(CreativeDocumentPresetBasedValidator.class);

    @Autowired
    private PresetsService presetsService;

    @Autowired
    private FileService fileService;

    @Autowired
    private AvatarsService avatarsService;

    @Autowired
    private InBannerVideoFilesService inBannerVideoFilesService;

    private Map<Integer, Preset> presetsById;
    private Map<String, Integer> presetIdByBundleName;

    @Override
    public void initialize(PresetBasedValidation constraintAnnotation) {
        List<Preset> presets = presetsService.getRawUntranslatedPresets();

        presetsById = StreamEx.of(presetsService.getRawUntranslatedPresets())
                .toMap(Preset::getId, identity());
        presetIdByBundleName = StreamEx.of(presets)
                .toMap(preset -> preset.getItems().get(0).getBundle().getName(), Preset::getId);
    }

    @Override
    public boolean isValid(CreativeDocument creative, ConstraintValidatorContext context) {
        Errors errors = new BeanPropertyBindingResult(creative, "item");

        if (creative.getAvailable()) {
            PresetItem presetItem = getPresetItem(errors, creative);
            if (presetItem != null) {
                validateSingleCreative(presetItem, creative, errors);
            }
        }

        if (!errors.hasErrors()) {
            return true;
        }

        processErrors(context, errors);
        return false;
    }

    /**
     * Получает описание креатива в исходном шаблоне по его ID.
     * Возвращает null если не может найти нужный шаблон или нужный item в этом шаблоне.
     */
    @Nullable
    private PresetItem getPresetItem(Errors errors, CreativeDocument creative) {
        if (creative.getData() == null || creative.getData().getBundle() == null
                || creative.getData().getBundle().getName() == null) {
            // дальше проверять нечего, ошибку на отсутствие этих полей должна выдать валиация по аннотациям этих полей
            return null;
        }

        Integer presetId = creative.getPresetId();

        // PresetId появился относительно недавно, поэтому большинство креативов его не имеют. Чтобы решить эту проблему
        // попытаемся его определить с помощью bundle name.
        if (presetId == null) {
            String bundleName = creative.getData().getBundle().getName();
            presetId = presetIdByBundleName.get(bundleName);
            creative.withPresetId(presetId);
        }

        if (presetId == null) {
            logger.error("Preset id not found for creative: {}", creative.getId());
        }

        Preset preset = presetsById.get(presetId);
        if (preset == null) {
            errors.rejectValue("presetId", "preset.not.found", "Preset not found");
            return null;
        }

        int itemId = (int) creative.getId();
        int anotherItemId = creative.getItemId();

        PresetItem presetItem = preset.getItemById(anotherItemId) == null ? preset.getItemById(itemId) :
                preset.getItemById(anotherItemId);

        if (presetItem == null) {
            logger.error(String.format("Preset item with id %d not found for preset %s", itemId, presetId));
            errors.rejectValue("presetId", "internal.error", "Preset item not found");
            return null;
        }
        return presetItem;
    }

    /**
     * Проверяет один креатив на соответствие условиям шаблона.
     *
     * @param presetItem Описание креатива в шаблоне
     * @param creative   Креатив
     * @param errors     Контейнер для ошибок
     */
    private void validateSingleCreative(@Nonnull PresetItem presetItem,
                                        @Nonnull CreativeDocument creative,
                                        @Nonnull Errors errors) {
        try {
            errors.pushNestedPath("data.");
            validateCreativeData(presetItem, creative.getData(), errors);
            validateElements(presetItem, creative, errors);
        } finally {
            errors.popNestedPath();
        }
    }

    /**
     * Проверяет данные креатива на соответствие условиям шаблона.
     *
     * @param presetItem   Описание креатива в шаблоне
     * @param creativeData Данные креатива
     * @param errors       Контейнер для ошибок
     */
    private void validateCreativeData(@Nonnull PresetItem presetItem,
                                      @Nonnull CreativeData creativeData,
                                      @Nonnull Errors errors) {
        List<CreativeDataValidator> creativeValidators = presetItem.getEffectiveValidators();
        StreamEx.of(nvl(creativeValidators, emptyList()))
                .forEach(v -> {
                    if (v instanceof FileServiceContainer) {
                        ((FileServiceContainer) v).setFileService(fileService);
                    }
                    if (v instanceof AvatarsServiceContainer) {
                        ((AvatarsServiceContainer) v).setAvatarsService(avatarsService);
                    }

                    if (v instanceof InBannerVideoContainer) {
                        ((InBannerVideoContainer)v).setVideoInBannerService(inBannerVideoFilesService);
                    }

                    v.validateIfSupported(creativeData, errors);
                });
    }

    /**
     * Проверяет элементы креатива.
     *
     * @param presetItem Описание креатива в шаблоне
     * @param creative   Креатив
     * @param errors     Контейнер для ошибок
     */
    private void validateElements(@Nonnull PresetItem presetItem,
                                  @Nonnull CreativeDocument creative,
                                  @Nonnull Errors errors) {
        List<Element> elements = creative.getData().getElements();
        Map<String, Integer> indexByType = EntryStream.of(elements)
                .mapValues(Element::getType)
                .invert()
                .toMap();
        Map<String, Element> elementByType = StreamEx.of(elements)
                .filter(Element::getAvailable)
                .toMap(Element::getType, identity());
        for (Element presetElement : presetItem.getElements()) {
            List<ElementValidator> validators = presetElement.getEffectiveValidators();
            Element element = elementByType.get(presetElement.getType());
            Integer elementIdx = indexByType.get(presetElement.getType());
            if (validators != null) {
                validateOneElement(elementIdx, element, validators, errors);
            }
        }
    }

    /**
     * Валидирует один {@link Element} из списка переданных элементов.
     */
    private void validateOneElement(Integer elementIdx, Element element, List<ElementValidator> validators,
                                    Errors errors) {
        try {
            if (elementIdx == null) {
                errors.pushNestedPath("elements.");
            } else {
                errors.pushNestedPath(String.format("elements[%d].", elementIdx));
            }
            StreamEx.of(validators)
                    .forEach(v -> v.validateIfSupported(element, errors));
        } finally {
            errors.popNestedPath();
        }
    }

    /**
     * Обрабатывает ошибки из переданного параметра {@link Errors} и передает их в контекст.
     */
    private void processErrors(ConstraintValidatorContext context, Errors errors) {
        HibernateConstraintValidatorContext ctx = context.unwrap(HibernateConstraintValidatorContext.class);
        for (ObjectError error : errors.getAllErrors()) {
            processExpressionVariables(ctx, error);

            ConstraintValidatorContext.ConstraintViolationBuilder builder = ctx
                    .buildConstraintViolationWithTemplate(error.getDefaultMessage());

            if (error instanceof FieldError) {
                builder.addNode(((FieldError) error).getField());
            }

            builder.addConstraintViolation();
        }
    }

    /**
     * Вытаскивает из ошибки ее аргументы и формирует из них значения для placeholder'ов в сообщении об ошибке.
     * Аргументы передаются массивом, длина которого должна быть кратна 2.
     * Сначала идет ключ-placeholder, за ним следует значение.
     */
    private void processExpressionVariables(HibernateConstraintValidatorContext ctx, ObjectError error) {
        Object[] arguments = Optional.ofNullable(error.getArguments()).orElse(new Object[0]);
        Preconditions.checkArgument(arguments.length % 2 == 0,
                "Arguments count should be even (key, value)");
        for (int i = 0; i < arguments.length; i += 2) {
            ctx.addMessageParameter(arguments[i].toString(), arguments[i + 1]);
        }
    }
}
