package ru.yandex.canvas.service.html5;

import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.google.common.collect.ImmutableList;
import one.util.streamex.StreamEx;
import org.apache.commons.io.FilenameUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.RestClientException;
import org.springframework.web.multipart.MultipartFile;

import ru.yandex.bolts.collection.Tuple2List;
import ru.yandex.canvas.Html5Constants;
import ru.yandex.canvas.exceptions.BadRequestException;
import ru.yandex.canvas.exceptions.InternalServerError;
import ru.yandex.canvas.exceptions.NotFoundException;
import ru.yandex.canvas.exceptions.SourceValidationError;
import ru.yandex.canvas.model.Size;
import ru.yandex.canvas.model.html5.CheckStatus;
import ru.yandex.canvas.model.html5.Source;
import ru.yandex.canvas.model.stillage.StillageFileInfo;
import ru.yandex.canvas.model.validation.Html5SizeValidator;
import ru.yandex.canvas.model.video.VideoFiles;
import ru.yandex.canvas.model.video.files.FileStatus;
import ru.yandex.canvas.model.video.files.FileType;
import ru.yandex.canvas.repository.html5.SourcesRepository;
import ru.yandex.canvas.service.DirectService;
import ru.yandex.canvas.service.MDSService;
import ru.yandex.canvas.service.SessionParams;
import ru.yandex.canvas.service.StillageService;
import ru.yandex.canvas.service.TankerKeySet;
import ru.yandex.canvas.service.screenshooters.Html5SourceScreenshooterHelperService;
import ru.yandex.canvas.service.video.MovieServiceInterface;
import ru.yandex.canvas.service.video.VideoCreativeType;
import ru.yandex.canvas.service.video.VideoMetaData;
import ru.yandex.direct.asynchttp.ParallelFetcherFactory;
import ru.yandex.direct.feature.FeatureName;
import ru.yandex.direct.screenshooter.client.model.ScreenShooterScreenshot;
import ru.yandex.misc.io.ByteArrayInputStreamSource;
import ru.yandex.misc.io.InputStreamSource;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static ru.yandex.canvas.Html5Constants.DEFAULT_MAX_FILE_SIZE;
import static ru.yandex.canvas.Html5Constants.FRONTPAGE_TAGS;
import static ru.yandex.canvas.Html5Constants.HTML5_ASYNC_INSPECTION_FEATURE;
import static ru.yandex.canvas.Html5Constants.HTML5_EXTERNAL_REQUESTS_SKIP_INSPECTION_FEATURE;
import static ru.yandex.canvas.Html5Constants.HTML5_PRESETS_BY_SIZE;
import static ru.yandex.canvas.Html5Constants.HTML5_VIDEO_ALLOWED_FEATURE;
import static ru.yandex.canvas.Html5Constants.MAX_CPM_BANNER_FILE_SIZE;
import static ru.yandex.canvas.Html5Constants.MAX_FILE_SIZE_BY_PRESET;
import static ru.yandex.canvas.Html5Constants.MAX_VIDEO_DURATION;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.CPM_PRICE;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.GENERATOR;
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.Html5Tag.PLAYABLE;
import static ru.yandex.canvas.service.SessionParams.SessionTag.CPM_YNDX_FRONTPAGE;
import static ru.yandex.canvas.service.html5.AdHtmlParser.NOT_NEED_IN_UPLOAD_HOSTS;
import static ru.yandex.canvas.service.html5.Html5Validator.getHtmlFileNames;
import static ru.yandex.direct.utils.CommonUtils.ifNotNull;
import static ru.yandex.direct.utils.CommonUtils.nvl;

public class Html5SourcesService {
    private static final Logger logger = LoggerFactory.getLogger(Html5SourcesService.class);

    private final StillageService stillageService;
    private final MDSService mdsService;
    private final SourcesRepository sourcesRepository;
    private final ParallelFetcherFactory parallelFetcherFactory;
    private final DirectService directService;
    private final MovieServiceInterface movieService;
    private final PhantomJsCreativesValidator phantomJsCreativesValidator;
    private final Html5SourceScreenshooterHelperService html5SourceScreenshooterHelperService;
    private final Html5SizeValidator html5SizeValidator;

    public Html5SourcesService(StillageService stillageService, MDSService mdsService,
                               SourcesRepository sourcesRepository,
                               ParallelFetcherFactory parallelFetcherFactory, DirectService directService,
                               MovieServiceInterface movieService,
                               PhantomJsCreativesValidator phantomJsCreativesValidator,
                               Html5SourceScreenshooterHelperService html5ScreenshooterHelperService,
                               Html5SizeValidator html5SizeValidator) {
        this.stillageService = stillageService;
        this.mdsService = mdsService;
        this.sourcesRepository = sourcesRepository;
        this.parallelFetcherFactory = parallelFetcherFactory;
        this.directService = directService;
        this.movieService = movieService;
        this.phantomJsCreativesValidator = phantomJsCreativesValidator;
        this.html5SourceScreenshooterHelperService = html5ScreenshooterHelperService;
        this.html5SizeValidator = html5SizeValidator;
    }

    private static boolean needToUploadHosts(String url) {
        try {
            String host = new URL(url).getHost();
            return !NOT_NEED_IN_UPLOAD_HOSTS.contains(host);
        } catch (MalformedURLException e) {
            return true;
        }
    }

    public Source uploadSource(MultipartFile file, long clientId,
                               SessionParams.Html5Tag productType) {
        Set<String> features = directService.getFeatures(clientId, null);
        validateMaxFileSize(file, productType, features, null);

        String ext = getFileExtension(file.getOriginalFilename());

        Source source;

        if (Html5Constants.VALID_IMAGE_EXTENSIONS.contains(ext)) {
            source = uploadImage(clientId, file, productType);
        } else if (ext.equals("zip")) {
            if (!Html5Constants.IS_ZIP_UPLOADING_SUPPORTED.contains(productType.getSessionTag())) {
                throw new SourceValidationError(TankerKeySet.ERROR.key("unsupported-mediatype"));
            }

            Html5Zip unpacked;

            try {
                unpacked = new Html5Zip(file.getBytes());
            } catch (IOException e) {
                throw new SourceValidationError(TankerKeySet.HTML5.key("not_valid_zip_file"));
            }

            //для прайсовых разные разрешённые размеры zip файла для расхлопа и не расхлопа.
            // Дополнительная проверка после распаковки архива
            validateMaxFileSize(file, productType, features, unpacked);

            source = uploadZip(clientId, file.getOriginalFilename(), unpacked,
                    new Html5Validator(unpacked, html5SizeValidator)
                            .validateUserZipCreative(productType, features),
                    productType,
                    !features.contains(HTML5_ASYNC_INSPECTION_FEATURE)
                            && !features.contains(HTML5_EXTERNAL_REQUESTS_SKIP_INSPECTION_FEATURE));
        } else {
            throw new SourceValidationError(TankerKeySet.HTML5.key("unsupported_file_format"));
        }

        sourcesRepository.insertSource(source);

        return source;
    }

    /**
     * Проверяет размер файла и кидает SourceValidationError если валидация не прошла
     */
    private static void validateMaxFileSize(MultipartFile file, SessionParams.Html5Tag productType,
                                            Set<String> features,
                                            Html5Zip unpacked) {
        var fileSizeLimit = Html5Constants.MAX_FILE_SIZE;
        if (productType != null && FRONTPAGE_TAGS.contains(productType)) {
            //Весь архив БЕЗ видео должен занимать до 1 МБ, отдельно видеоресурс может занимать до 20МБ. Т.е.
            //максимальный вес может составлять до 21МБ
            //Это размеры относятся ко всем креативам на морде
            fileSizeLimit = Html5Constants.FRONTPAGE_MAX_FILE_SIZE;
            if (productType == CPM_PRICE
                    && !Html5Constants.VALID_IMAGE_EXTENSIONS.contains(getFileExtension(file.getOriginalFilename()))
                    && (unpacked == null || unpacked != null && getHtmlFileNames(unpacked).size() > 1)//расхлоп
            ) {
                fileSizeLimit = Html5Constants.MAX_CPM_PRICE_VIDEO_FILE_SIZE + Html5Constants.FRONTPAGE_MAX_FILE_SIZE;
            }
            if (productType == CPM_PRICE && unpacked != null && getHtmlFileNames(unpacked).size() > 1) {
                //Распаковали, признали что расхлоп
                var totalSize = unpacked.files().stream()
                        .mapToInt(it -> unpacked.getFileContent(it).length)
                        .sum();
                var videos = unpacked.files().stream()
                        .filter(r -> Html5Constants.VALID_VIDEO_EXTENSIONS.contains(getFileExtension(r)))
                        .collect(Collectors.toList());
                var totalVideoSize = videos.stream()
                        .mapToInt(it -> unpacked.getFileContent(it).length)
                        .sum();
                if ((totalSize - totalVideoSize) > Html5Constants.FRONTPAGE_MAX_FILE_SIZE) {
                    throw new SourceValidationError(TankerKeySet.HTML5.formattedKey("cpm_price_file_is_too_big"));
                }
            }
        }
        if (productType != null && !FRONTPAGE_TAGS.contains(productType)//по фиче для неглавной поднимаем лимиты
                && features != null && features.contains(HTML5_VIDEO_ALLOWED_FEATURE)) {
            fileSizeLimit = Html5Constants.HTML5_VIDEO_ALLOWED_FILE_SIZE;
        }
        if (PLAYABLE == productType) {
            fileSizeLimit = Html5Constants.PLAYABLE_ALLOWED_FILE_SIZE;
        }
        if (GENERATOR == productType) {
            fileSizeLimit = Html5Constants.GENERATOR_ALLOWED_FILE_SIZE;
        }

        if (HTML5_CPM_BANNER == productType
                && features != null
                && features.contains(FeatureName.ALLOW_PROPORTIONALLY_LARGER_IMAGES.getName())) {
            fileSizeLimit = MAX_CPM_BANNER_FILE_SIZE;
        }

        if (file.getSize() > fileSizeLimit) {
            throwFileIsTooBigError(fileSizeLimit, file.getOriginalFilename());
        }
    }

    private static void throwFileIsTooBigError(Long fileSizeLimit, String filename) {
        long limitKb = fileSizeLimit / 1024;
        if (limitKb >= 1024L) {
            throw new SourceValidationError(TankerKeySet.HTML5.formattedKey("file_is_too_big_mb", filename, limitKb / 1024));
        }
        throw new SourceValidationError(TankerKeySet.HTML5.formattedKey("file_is_too_big", filename, limitKb));
    }

    /**
     * Валидирует, оборачивает в html и загружает в аватарницу переданную пользователем картинку.
     */
    Source uploadImage(long clientId, MultipartFile file, SessionParams.Html5Tag productType) {
        Set<String> features = directService.getFeatures(clientId, null);
        StillageFileInfo stillageFileInfo;
        try {
            stillageFileInfo = stillageService.uploadFile(file.getName(), file.getBytes());

            if (!Html5Constants.VALID_IMAGE_MIME_TYPES.containsKey(stillageFileInfo.getMimeType())
                    || !stillageFileInfo.getMetadataInfo().containsKey("height")
                    || !stillageFileInfo.getMetadataInfo().containsKey("width")
            ) {

                logger.info("Bad mime {}", stillageFileInfo.getMimeType());
                throw new BadRequestException(TankerKeySet.HTML5.key("unsupported_file_format"));
            }

        } catch (IOException e) {
            logger.error("error during upload", e);
            throw new InternalServerError();
        }

        //I don't know exactly yet what is inside - long or string..
        int width = (Integer) stillageFileInfo.getMetadataInfo().get("width");
        int height = (Integer) stillageFileInfo.getMetadataInfo().get("height");

        if (!html5SizeValidator.isSizesValid(List.of(Size.of(width, height)), features, productType)) {
            logger.info("Bad size {}/{}", width, height);

            throw new SourceValidationError(TankerKeySet.HTML5.formattedKey("unsupported_image_size",
                    html5SizeValidator.validSizesByProductTypeString(features)));
        }

        var preset = HTML5_PRESETS_BY_SIZE.get(Size.of(width, height));
        long maxFileSizeByPreset = preset == null ? DEFAULT_MAX_FILE_SIZE :
                MAX_FILE_SIZE_BY_PRESET.getOrDefault(preset, DEFAULT_MAX_FILE_SIZE);
        if (productType != null && FRONTPAGE_TAGS.contains(productType)) {
            maxFileSizeByPreset = Html5Constants.FRONTPAGE_MAX_FILE_SIZE;
        }
        if (productType == HTML5_CPM_BANNER
                && features != null
                && features.contains(FeatureName.ALLOW_PROPORTIONALLY_LARGER_IMAGES.getName())) {
            maxFileSizeByPreset = MAX_CPM_BANNER_FILE_SIZE;
        }
        if (file.getSize() > maxFileSizeByPreset) {
            throwFileIsTooBigError(maxFileSizeByPreset, file.getOriginalFilename());
        }

        String imageName = "image." + Html5Constants.VALID_IMAGE_MIME_TYPES.get(stillageFileInfo.getMimeType());

        Html5Zip zip;

        var baseSize = html5SizeValidator.getBaseSize(Size.of(width, height), features, productType);

        try {
            zip = Html5Zip.builder()
                    .addFile(imageName, file.getBytes())
                    .addFile("index.html", getHtmlForImage(baseSize.getWidth(), baseSize.getHeight(), imageName))
                    .build();
        } catch (IOException e) {
            logger.error("Generating html from image failed", e);
            throw new InternalServerError();
        }

        var html5ValidationResult = Html5Validator.Html5ValidationResult
                .buildImageHtml5ValidationResult(baseSize.getWidth(), baseSize.getHeight());

        Source source = uploadZip(clientId, file.getOriginalFilename(), zip, html5ValidationResult, productType, false);

        source.setSourceImageInfo(new Source.ImageStillageInfo(stillageFileInfo));

        return source;
    }

    private byte[] getHtmlForImage(long width, long height, String imageUrl) {
        Mustache mustache = new DefaultMustacheFactory().compile(Html5Constants.IMAGE_HTML_TEMPLATE_PATH);
        StringWriter writer = new StringWriter();

        Map<String, Object> scopes = new HashMap<>();
        scopes.put("width", width);
        scopes.put("height", height);
        scopes.put("image_url", imageUrl);

        mustache.execute(writer, scopes);
        writer.flush();

        return writer.toString().getBytes();
    }

    private static String getFileExtension(String filename) {
        return FilenameUtils.getExtension(filename).toLowerCase();
    }

    private Html5Zip getUploadableContent(Html5Zip content, String htmlFilename, SessionParams.Html5Tag productType,
                                          List<List<String>> htmlReplacements, String expandedHtmlName,
                                          VideoUploaded video) {
        Html5Zip.Builder builder = Html5Zip.builder();
        for (String name : content.files().stream()
                //не грузим оригинальный html и не грузим видео для баннеров на морде
                .filter(n -> filterUploadable(n, productType))
                .collect(Collectors.toList())) {
            builder.addFile(name, content.getFileContent(name));
        }
        //нужно сформировать правильный html для загрузки через iframe
        if (FRONTPAGE_TAGS.contains(productType)) {
            MdsHostedHtml.HtmlType type = expandedHtmlName == null ? MdsHostedHtml.HtmlType.SINGLE :
                    MdsHostedHtml.HtmlType.EXPANDED_MAIN;
            builder.addFile(htmlFilename,
                    new MdsHostedHtml(content.getFileAsUtf8String(htmlFilename), htmlReplacements, null, type)
                            .asHtml().getBytes());
        }
        if (expandedHtmlName != null) {
            //У cpm_price может быть два файла. Второй - расхлоп
            builder.addFile(expandedHtmlName,
                    new MdsHostedHtml(content.getFileAsUtf8String(expandedHtmlName),
                            htmlReplacements, video, MdsHostedHtml.HtmlType.EXPANDED_BIG).asHtml().getBytes());
        }
        return builder.build();
    }

    /**
     * Валидирует и загружает архив с html5-креативом.
     * Используется в т.ч. и для загрузки креативов, созданных из картинки.
     */
    private Source uploadZip(long clientId, String filename, Html5Zip content,
            Html5Validator.Html5ValidationResult html5ValidationResult,
            SessionParams.Html5Tag productType, boolean checkForExternalRequests) {
        String htmlFileName = html5ValidationResult.getHtml5FileName();

        List<List<String>> htmlReplacements = uploadLibrariesToStillage(html5ValidationResult.getPathsForUpload());

        if (checkForExternalRequests) {
            checkForExternalRequestsAndTrowError(content, htmlReplacements, htmlFileName);
        }

        VideoUploaded video = productType == SessionParams.Html5Tag.CPM_PRICE
                ? uploadVideoToStrm(content, clientId)
                : null;

        String mdsUrl = uploadZipToMDS(getUploadableContent(content, htmlFileName, productType, htmlReplacements,
                html5ValidationResult.getExpandedHtmlName(), video));
        String htmlBaseFileName = addDirToUrl(mdsUrl, htmlFileName);

        logger.info("Base path {}", htmlBaseFileName);

        StillageFileInfo stillageFileInfo;
        try {
            stillageFileInfo = stillageService.uploadFile(filename, content.toArchive());
        } catch (IOException e) {
            logger.error("stillageService.uploadFile failed", e);
            throw new InternalServerError();
        }

        var preset = Html5Constants.HTML5_PRESETS_BY_SIZE
                .get(new Size(html5ValidationResult.getWidth(), html5ValidationResult.getHeight()));

        Integer presetId = ifNotNull(preset, Html5Preset::getId);

        Source source = new Source().setClientId(clientId)
                .setPresetId(presetId)
                .setDate(Date.from(Instant.now()))
                .setArchive(false)
                .setName(filename)
                .setWidth(html5ValidationResult.getWidth())
                .setHeight(html5ValidationResult.getHeight())
                .setPreviewUrl(null)
                .setScreenshotUrl(null)
                .setScreenshotIsDone(false)
                .setUrl(stillageFileInfo.getUrl())
                .setBasePath(htmlBaseFileName)
                .setStillageInfo(new Source.ZipStillageInfo(stillageFileInfo))
                .setHtmlFilename(htmlFileName)
                .setHtmlUrl(FRONTPAGE_TAGS.contains(productType) ? (mdsUrl + htmlFileName) : null)
                .setExpandedHtmlUrl(html5ValidationResult.getExpandedHtmlName() == null ? null
                        : mdsUrl + html5ValidationResult.getExpandedHtmlName())
                .setBgrcolor(html5ValidationResult.getBgrcolor())
                .setValidationStatus(checkForExternalRequests ? CheckStatus.VALID : CheckStatus.READY)
                .setHtmlReplacements(htmlReplacements);
        if (video != null) {
            source.setVideoDuration(video.getDuration())
                    .setVideoUrl(video.getUrl());
        }

        try {
            source.setPreviewUrl(publishPreview(source, htmlFileName, content, productType.getSessionTag()));
        } catch (IOException | URISyntaxException ex) {
            logger.error("publishPreview failed: {}", ex);
            throw new InternalServerError();
        }

        // Если не удалось сделать скриншот, то в базу записывается null.
        ScreenShooterScreenshot screenshot = html5SourceScreenshooterHelperService.getScreenshot(
                source, null, clientId);

        if (screenshot != null) {
            source.setScreenshotUrl(screenshot.getUrl());
            source.setScreenshotIsDone(screenshot.getIsDone());
            source.setScreenshotUrlMainBanner(screenshot.getUrlMainBanner());
        }

        return source;
    }

    public static class FileUploaded {
        private String path;

        private String contentUrl;

        public FileUploaded(String path, String contentUrl) {
            this.path = path;
            this.contentUrl = contentUrl;
        }

        public String getPath() {
            return path;
        }

        public String getContentUrl() {
            return contentUrl;
        }
    }

    private void checkForExternalRequestsAndTrowError(Html5Zip content, List<List<String>> htmlReplacements,
                                                      String htmlFileName) {
        List<String> externalRequests = checkForExternalRequests(content, htmlReplacements, htmlFileName);
        if (externalRequests.size() > 0) {
            String paths = String.join(", ", externalRequests);
            logger.warn("Not allowed external requests found", new SourceValidationError(paths));
            var error = TankerKeySet.HTML5.formattedKey("has_not_allowed_external_paths", paths);
            throw new SourceValidationError(error);
        }
    }

    public List<String> checkForExternalRequests(Html5Zip content, List<List<String>> htmlReplacements,
                                                 String htmlFileName) {
        var uploadedFiles = new ArrayList<FileUploaded>(content.files().size());
        try {
            for (var fullName : content.files()) {
                var fileContent = nvl(getHtmlContentWithReplacements(htmlReplacements, content, fullName),
                        content.getFileContent(fullName));
                var fileName = FilenameUtils.getName(fullName);
                if (fullName.equals(htmlFileName) && !fileName.equals("index.html")) {
                    fileName = "index.html";
                }
                var stillageFileInfo = stillageService.uploadFile(fileName, fileContent);
                uploadedFiles.add(new FileUploaded(fileName, stillageFileInfo.getUrl()));
            }
        } catch (RestClientException e) {
            logger.error("Upload to stillage for the task of checking external requests failed", e);
            return emptyList();
        }

        List<String> externalRequests = phantomJsCreativesValidator.checkForExternalRequests(uploadedFiles);
        return StreamEx.of(externalRequests)
                .filter(Html5SourcesService::needToUploadHosts)
                .toList();
    }

    private byte[] getHtmlContentWithReplacements(List<List<String>> htmlReplacements, Html5Zip content,
                                                  String fileName) {
        if (!htmlReplacements.isEmpty()
                && Html5Constants.ALLOWED_HTML_EXTENSIONS.contains(getFileExtension(fileName))) {
            var htmlFile = content.getFileAsUtf8String(fileName);
            var doc = Jsoup.parse(htmlFile);
            for (List<String> lst : htmlReplacements) {
                var srcLib = lst.get(0);
                var cachedLib = lst.get(1);
                for (Element el : doc.select("script[src=" + srcLib + "]")) {
                    el.attr("src", cachedLib);
                }
            }
            return doc.toString().getBytes();
        } else {
            return null;
        }
    }

    public void downloadZipContent(Source source) throws InterruptedException, IOException {
        downloadZipContent(singletonList(source));
    }

    public void downloadZipContent(List<Source> sources) throws InterruptedException, IOException {
        ZipDownloader zipDownloader = new ZipDownloader(parallelFetcherFactory, sources);
        zipDownloader.download();
    }

    private boolean filterUploadable(String name, SessionParams.Html5Tag productType) {
        String extension = getFileExtension(name);
        return !Html5Constants.ALLOWED_HTML_EXTENSIONS.contains(extension)
                && (!Html5Constants.VALID_VIDEO_EXTENSIONS.contains(extension) || !FRONTPAGE_TAGS.contains(productType));
    }

    public static class VideoUploaded {
        private Double duration;
        private String url;

        public VideoUploaded(Double duration, String url) {
            this.duration = duration;
            this.url = url;
        }

        public Double getDuration() {
            return duration;
        }

        public String getUrl() {
            return url;
        }
    }

    public VideoUploaded uploadVideoToStrm(Html5Zip content, Long clientId) {
        for (String name : content.files().stream()
                .filter(r -> Html5Constants.VALID_VIDEO_EXTENSIONS.contains(getFileExtension(r)))
                .collect(Collectors.toList())) {
            StillageFileInfo info = stillageService.uploadFile(name, content.getFileContent(name));
            if (info.getMetadataInfo().containsKey("duration")
                    && info.getMetadataInfo().get("duration") instanceof Double) {
                Double duration = (Double) info.getMetadataInfo().get("duration");
                if (duration != null && duration > MAX_VIDEO_DURATION) {
                    throw new SourceValidationError(TankerKeySet.HTML5.key("max_video_duration"));
                }
                VideoMetaData videoMetaData = new ObjectMapper().convertValue(info.getMetadataInfo(),
                        VideoMetaData.class);
                if (videoMetaData.getVideoStreams() == null || videoMetaData.getVideoStreams().isEmpty()) {
                    throw new SourceValidationError(TankerKeySet.VIDEO_VALIDATION_MESSAGES.key(
                            "invalid_video_file_format"));
                }
                VideoMetaData.VideoStreamInfo videoStreamInfo = videoMetaData.getVideoStreams().get(0);
                if (videoStreamInfo.getHeight() == null || videoStreamInfo.getHeight() < 240) {
                    throw new SourceValidationError(TankerKeySet.VIDEO_VALIDATION_MESSAGES.key("invalid_video_height"));
                }
                try {
                    String fileId = movieService.upload(content.getFileContent(name), name,
                            clientId, VideoCreativeType.HTML5, null)
                            .getVideoSource().getId();
                    //дождаться конвертации MovieService.getFileById
                    VideoFiles file = movieService.getFileById(fileId, FileType.VIDEO, clientId);
                    while (file.getStatus() == FileStatus.CONVERTING) {
                        Thread.sleep(2000);
                        file = movieService.getFileById(fileId, FileType.VIDEO, clientId);
                    }
                    //отдать самый большой формат
                    VideoFiles.VideoFormat format = getMaxVideoFormat(file);
                    return format == null ? null : new VideoUploaded(duration, format.getUrl());
                } catch (Exception e) {
                    logger.error("error during upload video", e);
                    throw new InternalServerError(e);
                }
            }
        }
        return null;
    }

    private static VideoFiles.VideoFormat getMaxVideoFormat(VideoFiles file) {
        if (file.getFormats() == null) {
            return null;
        }
        return file.getFormats().stream()
                .filter(it -> it.getMimeType() != null && it.getMimeType().equals("video/mp4"))
                .max(comparatorLargestWidthVideoFormat())
                .orElse(null);
    }

    protected static Comparator<VideoFiles.VideoFormat> comparatorLargestWidthVideoFormat() {
        return Comparator.comparingInt(it -> Integer.parseInt(it.getWidth()));
    }

    public List<List<String>> uploadLibrariesToStillage(List<String> libraries) {
        List<List<String>> result = new ArrayList<>();

        for (String forUpload : libraries) {
            URL url;

            try {
                url = new URL(forUpload);
            } catch (MalformedURLException e) {
                throw new SourceValidationError("Incorrect url: " + forUpload);
            }

            String libName = Paths.get(url.getPath()).getFileName().toString();

            if (libName == null) {
                throw new InternalServerError();
            }

            StillageFileInfo stillageFileInfo = stillageService.uploadFile(libName, url);
            logger.info("Uploaded whitelisted {} into {}", forUpload, stillageFileInfo.getUrl());

            result.add(ImmutableList.of(forUpload, stillageFileInfo.getUrl()));
        }

        return result;
    }

    public String addDirToUrl(String baseUrl, String additional) {
        if (additional.length() > 0 && additional.substring(0, 1).equals("/")) {
            additional = additional.substring(1);
        }

        Path htmlPath = Paths.get(additional);
        Path dirName = htmlPath.getParent();

        if (dirName != null && dirName.getNameCount() != 0) {
            baseUrl += dirName + "/";
        }

        return baseUrl;
    }

    private String publishPreview(Source source, String htmlFileName, Html5Zip html5Zip,
                                  SessionParams.SessionTag productType) throws IOException, URISyntaxException {
        if (source.getHtmlUrl() != null) {
            //Для баннеров на Главной можно использовать html в MDS для скриншота и превью
            return source.getHtmlUrl();
        }
        boolean isCloseButtonPresent = productType != CPM_YNDX_FRONTPAGE;

        Preview preview = new Preview(source.getHtmlReplacements(), html5Zip.getFileAsUtf8String(htmlFileName),
                isCloseButtonPresent, directService.getFeatures(source.getClientId(), null),
                SessionParams.getHtml5SessionTag(productType));

        String fileName = UUID.randomUUID() + ".html";

        MDSService.MDSDir res = mdsService.uploadMultiple(Tuple2List.fromPairs(fileName,
                new ByteArrayInputStreamSource(preview.asHtml(source.getWidth(), source.getHeight(),
                        source.getBasePath()).getBytes())));

        return res.getURL();
    }

    public String reuploadIndexHtml(Source source) throws IOException, InterruptedException {
        //сервис только для креативов для морды
        downloadZipContent(source);
        Html5Zip html5Zip = source.unzipArchiveContent();
        String mdsUrl = uploadZipToMDS(getUploadableContent(html5Zip, source.getHtmlFilename(),
                HTML5_CPM_YNDX_FRONTPAGE, source.getHtmlReplacements(), null, null));
        return mdsUrl + source.getHtmlFilename();
    }

    private String uploadZipToMDS(Html5Zip content) {
        Tuple2List<String, InputStreamSource> fileSourcesList = new Tuple2List<>(new ArrayList<>());

        for (String name : content.files()) {
            //MDS disallows empty files and dirs explicitly
            if (content.getFileContent(name).length > 0) {
                fileSourcesList.add(name, new ByteArrayInputStreamSource(content.getFileContent(name)));
            }
        }

        if (fileSourcesList.isEmpty()) {
            fileSourcesList.add(".placeholder", new ByteArrayInputStreamSource("PLACEHOLDER".getBytes()));
        }

        try {
            return mdsService.uploadMultiple(fileSourcesList).getDirUrl();
        } catch (Exception e) {
            logger.error("uploadMultiple failed", e);
            throw new InternalServerError();
        }
    }

    public Source getSource(String id, long clientId) {
        Source src = sourcesRepository.getSourceById(id, clientId);

        if (src == null) {
            throw new NotFoundException();
        }

        return src;
    }

    public void deleteSource(String id, long clientId) {
        sourcesRepository.archiveSourceById(id, clientId);
    }

    public List<Source> getSources(Long clientId, String... sourceIds) {
        return sourcesRepository.getSourcesByIds(clientId, sourceIds);
    }
}
