package ru.yandex.canvas.service;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.validation.constraints.NotNull;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.canvas.model.elements.Element;
import ru.yandex.canvas.model.presets.Preset;
import ru.yandex.canvas.model.presets.PresetSelectionCriteria;
import ru.yandex.canvas.model.presets.PresetTag;

import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableMap;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.springframework.context.i18n.LocaleContextHolder.getLocale;
import static ru.yandex.canvas.model.Util.deepcopy;
import static ru.yandex.canvas.service.TankerKeySet.MEDIASET_URL;
import static ru.yandex.canvas.service.TankerKeySet.PRESETS;

/**
 * @author skirsanov
 */
public class PresetsService {
    private static final Logger logger = LoggerFactory.getLogger(PresetsService.class);

    public static final Set<Integer> IN_BANNER_PRESET_IDS = ImmutableSet.of(21, 22, 23, 24, 25, 26, 27);
    public static final Set<Integer> DEFAULT_BANNER_PRESET_IDS = ImmutableSet.of(1, 2, 3, 4);
    public static final Set<Integer> GEO_PIN_PRESET_IDS = ImmutableSet.of(11);


    private static final String PRESETS_DIRECTORY = "presets";
    private final Map<PresetTag, Set<Integer>> presetIdsByTag;

    // loaded from presets/*.json
    private final List<Preset> presets = new ArrayList<>();

    private final ObjectMapper objectMapper;
    private final DirectService directService;

    public PresetsService(ObjectMapper objectMapper, DirectService directService) {
        this.objectMapper = objectMapper;
        this.directService = directService;
        loadPresets(objectMapper);

        if (presets.isEmpty()) {
            throw new RuntimeException("Not presets were loaded, check config and resources");
        }

        Map<PresetTag, Set<Integer>> presetIdsByTag = new HashMap<>();

        presets.forEach(preset -> preset.getTags().forEach(tag ->
                presetIdsByTag.computeIfAbsent(tag, t -> new HashSet<>()).add(preset.getId())));

        this.presetIdsByTag = unmodifiableMap(presetIdsByTag);
    }

    /**
     * Загружает preset'ы из ресурсов.
     */
    private void loadPresets(ObjectMapper objectMapper) {
        try {

            URI presetsUri = Resources.getResource(PRESETS_DIRECTORY).toURI();
            Path presetsPath;

            if (presetsUri.getScheme().equalsIgnoreCase("jar")) {
                FileSystem fileSystem;

                try {
                    fileSystem = FileSystems.getFileSystem(presetsUri);
                } catch (FileSystemNotFoundException e) {
                    fileSystem = FileSystems.newFileSystem(presetsUri, emptyMap());
                }

                // TODO presetsPath = fileSystem.getPath("BOOT-INF/classes/" + PRESETS_DIRECTORY);
                presetsPath = fileSystem.getPath(PRESETS_DIRECTORY);
            } else {
                presetsPath = Paths.get(presetsUri);
            }

            Files.walk(presetsPath)
                    .filter(Files::isRegularFile)
                    .forEach(presetPath -> {
                        try {
                            Preset preset = objectMapper.readValue(Files.readString(presetPath), Preset.class);

                            Collections.sort(preset.getItems());

                            presets.add(preset);
                        } catch (IOException e) {
                            logger.error("can not load preset file: " + presetPath, e);
                            throw new RuntimeException("can not load preset file: " + presetPath, e);
                        }
                    });
            presets.sort(comparing(Preset::hasAdaptiveSupport).reversed().thenComparing(Preset::getId));

            // Check unique ids
            Set<Integer> presetIds = presets.stream().map(Preset::getId).collect(toSet());
            if (presetIds.size() != presets.size()) {
                throw new RuntimeException("Some presets have duplicate IDs");
            }
        } catch (IOException | URISyntaxException e) {
            logger.error("can not load presets directory", e);
            throw new RuntimeException("can not load presets directory", e);
        }
    }

    private Preset localize(@NotNull Preset original, @NotNull Locale locale) {
        Preset result = deepcopy(objectMapper, original, Preset.class);
        result.setThumbnail(PRESETS.key(result.getThumbnail(), locale));
        result.getItems().forEach(presetItem -> {
            presetItem.getElements().forEach(element -> {
                Element.Options options = element.getOptions();
                if (options instanceof Element.ColoredTextOptions) {
                    Element.ColoredTextOptions textOptions = ((Element.ColoredTextOptions) options);
                    textOptions.setPlaceholder(PRESETS.key(textOptions.getPlaceholder(), locale));
                }
            });
            presetItem.getMediaSets().forEach((s, mediaSet) ->
                    mediaSet.getItems().forEach(mediaSetItem -> {
                        mediaSetItem.getItems().forEach(mediaSetSubItem ->
                                mediaSetSubItem.setUrl(MEDIASET_URL.keyOmitLogging(mediaSetSubItem.getUrl(), locale)));
                    }));
        });
        return result;
    }

    /**
     * Возвращает список всех шаблонов с локализацией и фильтрацией адаптивных
     * креативов, если клиент не имеет к ним доступа
     */
    public List<Preset> getList(boolean useAdaptive) {
        return getList(PresetSelectionCriteria.any(useAdaptive), null, null);
    }

    public List<Preset> getList(PresetSelectionCriteria selectionCriteria, Long clientId, Boolean inBannerFlag) {
        Locale locale = getLocale();

        Set<String> features = null;

        if (clientId != null) {
            features = directService.getFeatures(clientId, null);
        }
        var comparator = Comparator.comparingInt(Preset::getId);
        if (inBannerFlag != null && inBannerFlag) {
            comparator = Comparator.comparing(p -> !IN_BANNER_PRESET_IDS.contains(p.getId()));
        }

        Set<String> finalFeatures = features;
        return presets.stream()
                .filter(p -> !Sets.intersection(selectionCriteria.getTags(), p.getTags()).isEmpty())
                .filter(p -> selectionCriteria.useAdaptive() || !p.hasAdaptiveSupport())
                .filter(p -> finalFeatures == null || finalFeatures.contains("in_banner_creatives_support") || !IN_BANNER_PRESET_IDS.contains(p.getId()))
                .filter(p -> finalFeatures == null || !finalFeatures.contains("only_adaptive_creatives") || !DEFAULT_BANNER_PRESET_IDS.contains(p.getId()))
                .filter(p -> inBannerFlag == null || inBannerFlag || !IN_BANNER_PRESET_IDS.contains(p.getId()))
                .map(p -> localize(p, locale))
                .sorted(comparator)
                .collect(toList());
    }

    /**
     * Возвращает список всех шаблонов. Вне зависимости от настроек клиента.
     * Нужно для прайсовых кампаний.
     */
    public List<Preset> getAll() {
        return presets;
    }

    public Optional<Preset> getById(int id, PresetSelectionCriteria selectionCriteria, Long clientId) {
        return getList(selectionCriteria, clientId, null).stream().filter(p -> p.getId() == id).findAny();
    }

    public Optional<Preset> getById(int id) {
        Locale locale = getLocale();
        return presets.stream()
                .filter(p -> p.getId() == id)
                .map(p -> localize(p, locale))
                .findAny();
    }

    /**
     * Возвращает список шаблонов без переводов и дополнительной обработки.
     */
    public List<Preset> getRawUntranslatedPresets() {
        return Collections.unmodifiableList(presets);
    }

    public Map<PresetTag, Set<Integer>> getPresetIdsByTag() {
        return presetIdsByTag;
    }
}
