package ru.yandex.canvas.service.video;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.HttpClientErrorException;

import ru.yandex.canvas.exceptions.SandboxConvertionException;
import ru.yandex.canvas.model.avatars.AvatarsPutCanvasResult;
import ru.yandex.canvas.model.stillage.StillageFileInfo;
import ru.yandex.canvas.model.stillage.StillageInfoConverter;
import ru.yandex.canvas.model.video.CleanupOldConversionsResult;
import ru.yandex.canvas.model.video.VideoFiles;
import ru.yandex.canvas.model.video.files.FileStatus;
import ru.yandex.canvas.repository.video.VideoFilesRepository;
import ru.yandex.canvas.service.AvatarsService;
import ru.yandex.canvas.service.DirectService;
import ru.yandex.canvas.service.SandBoxService;
import ru.yandex.canvas.service.StillageService;
import ru.yandex.canvas.service.VideoLimitsInterface;
import ru.yandex.canvas.service.sandbox.ClientTag;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static ru.yandex.canvas.VideoConstants.CANVAS_CMS_ENCODE_FEATURE;
import static ru.yandex.canvas.VideoConstants.CANVAS_RANGE_RATIO_CPC_FEATURE;
import static ru.yandex.canvas.VideoConstants.CANVAS_RANGE_RATIO_CPC_FEATURE_ALLOWED_CREATIVE_TYPES;
import static ru.yandex.canvas.VideoConstants.CANVAS_RANGE_RATIO_FEATURE;
import static ru.yandex.canvas.VideoConstants.CANVAS_VIDEO_ANY_SIZE_ALLOWED_FEATURE;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_CAPTURE_START;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_CAPTURE_STOP;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_ENCODING_PRESET;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_FIRST_RESOLUTION;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_FRAMERATE;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_LOUDNORM;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_OUTPUT_FORMAT;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_RESOLUTIONS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_SEGMENT_TIME;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_SETPTS_PTS_STARTPTS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_THUMBNAILS_SCENE;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_THUMBNAIL_T;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FFMPEG_TWO_PASS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.FORCE_DURATION_CALCULATION;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.KEEP_ASPECT_RATIO;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.S3_BUCKET;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.S3_DIR;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.S3_KEY_PREFIX;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.S3_TESTING;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.STRM_EMBED_READY;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.WEB_HOOK_URL;

public class VideoFileUploadService extends FileUploadService<VideoMetaData>
        implements VideoFileUploadServiceInterface {
    private static final Logger logger = LoggerFactory.getLogger(VideoFileUploadService.class);

    // Полный набор тэгов Job-ы: GENERIC & INTEL_E5_2650 & LINUX & ~IPV4
    // Но нам надо проставить только INTEL_E5_2650, остальное таска проставит сама
    private static final String STRM_JOB_CLIENT_TAGS = ClientTag.INTEL_E5_2650.build();

    private final SandBoxService sandBoxService;
    private final String hookSecret;
    private final String canvasVaApiBaseUrl; //CANVAS_VA_API_BASE_URL
    private final VideoLimitsService videoLimitsService;
    private final AvatarsService avatarsService;
    private final DirectService directService;
    private final VideoPresetsService videoPresetsService;
    private final StillageService stillageService;
    private final StillageInfoConverter stillageInfoConverter;
    private final VhService vhClient;
    private final VideoGeometryService videoGeometryService;

    public VideoFileUploadService(SandBoxService sandBoxService, String hookSecret, String canvasVaApiBaseUrl,
                                  VideoFilesRepository videoFilesRepository,
                                  VideoLimitsService videoLimitsService, AvatarsService avatarsService,
                                  DirectService directService, VideoPresetsService videoPresetsService,
                                  StillageService stillageService, StillageInfoConverter stillageInfoConverter,
                                  VhService vhClient, VideoGeometryService videoGeometryService) {
        super(videoFilesRepository);
        this.sandBoxService = sandBoxService;
        this.hookSecret = hookSecret;
        this.canvasVaApiBaseUrl = canvasVaApiBaseUrl;
        this.videoLimitsService = videoLimitsService;
        this.avatarsService = avatarsService;

        this.directService = directService;
        this.videoPresetsService = videoPresetsService;
        this.stillageService = stillageService;
        this.vhClient = vhClient;
        this.stillageInfoConverter = stillageInfoConverter;
        this.videoGeometryService = videoGeometryService;
    }

    private String makeHookUrl(String dbId) {
        return String.format("%s/files/%s/event?secret=%s", canvasVaApiBaseUrl, dbId, hookSecret);
    }

    private SandBoxService.SandboxCreateRequest makeSandboxRequest(VideoFiles dbRecord, boolean needFast) {
        var priority = needFast ? "SERVICE" : "BACKGROUND";
        var subPriority = needFast ? "HIGH" : "LOW";
        if (dbRecord.getId() == null) {
            throw new IllegalArgumentException(
                    "dbRecord must be already inserted before createVideoConvertionTask is called");
        }

        var creativeType = dbRecord.getCreativeType();

        //по фиче разрешать нестандартные форматы и не добавлять поля
        Set<String> features = directService.getFeatures(dbRecord.getClientId(), null);
        boolean keepAspectRatio =
                features.contains(CANVAS_VIDEO_ANY_SIZE_ALLOWED_FEATURE) ||
                        features.contains(CANVAS_RANGE_RATIO_FEATURE) ||
                        (features.contains(CANVAS_RANGE_RATIO_CPC_FEATURE) &&
                                CANVAS_RANGE_RATIO_CPC_FEATURE_ALLOWED_CREATIVE_TYPES.contains(creativeType));

        Integer ffmpegFrameRate = null;
        String ffmpegOutputFormat = "hls,mp4,webm";
        Boolean strmEmbedReady = true;
        Double ffmpegCaptureStop = dbRecord.getDuration();
        String ffmpegResolution = null;

        if (creativeType == VideoCreativeType.CPM_OUTDOOR) {
            ffmpegFrameRate = 25;
            ffmpegOutputFormat = "hls,mp4";
            strmEmbedReady = null;
            ffmpegResolution = DirectFfmpegResolutions.OUTDOOR.getConvertibleResolutionsAsString(
                    dbRecord.getRatio(),
                    getFirstStreamWidth(dbRecord.getStillageFileInfo()).intValue(),
                    getFirstStreamHeight(dbRecord.getStillageFileInfo()).intValue());
        } else if (creativeType == VideoCreativeType.CPM_INDOOR) {
            ffmpegFrameRate = 25;
            ffmpegOutputFormat = "hls,mp4,webm";
            strmEmbedReady = null;
            ffmpegResolution = DirectFfmpegResolutions.INDOOR.getAllResolutionsAsString(dbRecord.getRatio());
        } else if (creativeType == VideoCreativeType.HTML5) {
            ffmpegOutputFormat = "mp4";
            keepAspectRatio = true;
            strmEmbedReady = null;
        } else if (creativeType == VideoCreativeType.IN_BANNER) {
            ffmpegResolution = DirectFfmpegResolutions.IN_BANNER.getAllResolutionsAsString(dbRecord.getRatio());
            strmEmbedReady = null;
            keepAspectRatio = true;
        }

        if (keepAspectRatio) {
            strmEmbedReady = null;
        }

        SandBoxService.SandboxCreateRequest request = new SandBoxService.SandboxCreateRequest();

        request
                .setPriority(priority, subPriority)
                .addCustomField(SandBoxService.VideoTaskExtraFields.URL, dbRecord.getUrl())
                .addCustomField(WEB_HOOK_URL, makeHookUrl(dbRecord.getId()))
                .addCustomField(S3_KEY_PREFIX, dbRecord.getStrmPrefix())
                .addCustomField(S3_DIR, "get-canvas")
                .addCustomField(S3_TESTING, false)
                .addCustomField(S3_BUCKET, "vh-canvas-converted")
                .addCustomField(FFMPEG_OUTPUT_FORMAT, ffmpegOutputFormat)
                .addCustomField(FFMPEG_THUMBNAILS_SCENE, true)
                .addCustomField(FFMPEG_CAPTURE_STOP, ffmpegCaptureStop)
                .addCustomField(FFMPEG_TWO_PASS, false)
                .addCustomField(FFMPEG_CAPTURE_START, 0.0)
                .addCustomField(FFMPEG_SEGMENT_TIME, 3.0) // CANVAS-1038
                .addCustomField(FFMPEG_THUMBNAIL_T, 0.5)
                .addCustomField(FFMPEG_ENCODING_PRESET, "veryslow")
                .addCustomField(FFMPEG_FIRST_RESOLUTION, true)
                .addCustomField(KEEP_ASPECT_RATIO, keepAspectRatio)
                .addCustomField(FFMPEG_LOUDNORM, true);

        if (strmEmbedReady != null) {
            request.addCustomField(STRM_EMBED_READY, strmEmbedReady);
        }

        if (ffmpegFrameRate != null) {
            request.addCustomField(FFMPEG_FRAMERATE, (double) ffmpegFrameRate);
        }

        if (ffmpegResolution != null) {
            request.addCustomField(FFMPEG_RESOLUTIONS, ffmpegResolution);
        } else {
            logger.info("ffmpegResolution was not computed for creativeType = {}. unknown ratio: {}?", creativeType,
                    dbRecord.getRatio());
        }

        if (creativeType == VideoCreativeType.CPM_OUTDOOR) {
            request.addCustomField(FORCE_DURATION_CALCULATION, true);
            request.addCustomField(FFMPEG_SETPTS_PTS_STARTPTS, true);
        }

        request.setRequirements(new SandBoxService.SandboxCreateRequest.SandboxRequirements()
                .setClientTags(STRM_JOB_CLIENT_TAGS));

        request.setOwner("CANVAS");
        request.setType("STRM_VIDEO_CONVERT");

        return request;
    }

    private static double roundVideoDuration(VideoCreativeType type, double duration) {
        switch (type) {
            case CPM:
            case NON_SKIPPABLE_CPM:
            case PRICE_CAMPS_NON_SKIPPABLE_CPM:
            case PRICE_CAMPS_CPM:
            case CPM_YNDX_FRONTPAGE:
            case CPM_INDOOR:
                return duration;
            case CPM_OUTDOOR:
                throw new IllegalArgumentException("There is special set with allowed durations for outdoor.");
            case MOBILE_CONTENT:
                return Math.floor(duration);
            default:
                return Math.ceil(duration);
        }
    }

    private static double roundOutdoorVideoDuration(VideoLimitsInterface limits,
                                                    VideoMetaData.VideoStreamInfo videoStreamInfo) {
        Ratio ratio = new Ratio(videoStreamInfo.getWidth(), videoStreamInfo.getHeight());
        String ratioString = ratio.toString();
        double videoDuration = videoStreamInfo.getDuration();

        Map<String, Object> ratioLimits = (Map) limits.getDurationLimitsByRatio().get(ratioString);
        checkNotNull(ratioLimits, "Not found outdoor limits for %s ratio", ratioString);
        List<Number> availableDurations = (List) ratioLimits.get("durations");
        checkNotNull(availableDurations, "Not found outdoor durations limit for %s ratio", ratioString);

        Optional<Number> roundedDuration = availableDurations.stream()
                .filter(duration -> {
                    double minDuration = duration.doubleValue() - limits.getDurationDelta();
                    double maxDuration = duration.doubleValue() + limits.getDurationDelta();
                    return videoDuration >= minDuration && videoDuration <= maxDuration;
                })
                .findFirst();
        checkState(roundedDuration.isPresent(), "Not able to determine outdoor video duration");

        return roundedDuration.get().doubleValue();
    }

    @Override
    public void beforeInsert(VideoFiles record, StillageFileInfo info, VideoMetaData videoMetaData, Long presetId) {
        //TODO check everything for existence
        VideoMetaData.VideoStreamInfo videoStreamInfo = videoMetaData.videoStreams.get(0);

        Ratio ratio = new Ratio(videoStreamInfo.getWidth(), videoStreamInfo.getHeight());
        record.setRatio(ratio.toString());
        record.setRatioPercent(ratio.ratioPercent());
        record.setDuration(estimatedDuration(record, videoMetaData, presetId));
        record.setOverlayColor("#000000");
        record.setSubCategories(Collections.emptyList());
        // Видео, загруженные через видеоконструктор, навсегда остаются со статусом NEW. Это нужно, чтобы они не попадали
        // в выборку видосов для использования в креативах, так как конвертацию мы для них не запускаем.
        record.setStatus(FileStatus.NEW);

        /// Скриншот делаем в ручную через stillage
        record.setThumbnail(makeThumbnail(record));
    }

    private VideoFiles.VideoThumbnail makeThumbnail(VideoFiles record) {
        try {
            StillageFileInfo stillageResult = stillageService.uploadScreenshotForVideoUrlInternal(
                    "screenshot_" + record.getStillageFileInfo().getId() + ".jpg", record.getUrl());
            return getThumbnailFromStillageResult(stillageResult);
        } catch (Exception e) {
            return null;
        }
    }

    private double estimatedDuration(VideoFiles record, VideoMetaData videoMetaData, Long presetId) {
        VideoMetaData.VideoStreamInfo videoStreamInfo = videoMetaData.videoStreams.get(0);
        if (record.getCreativeType() != null && record.getCreativeType() == VideoCreativeType.HTML5) {
            return videoStreamInfo.getDuration();//для html5 не округлем. Они без pcode показываются
        }

        Set<String> features = directService.getFeatures(record.getClientId(), null);
        VideoLimitsInterface limits = videoLimitsService.getLimits(record.getCreativeType(), features, presetId);
        double estimatedDuration;

        if (record.getCreativeType() == VideoCreativeType.CPM_OUTDOOR) {
            estimatedDuration = roundOutdoorVideoDuration(limits, videoStreamInfo);
        } else {
            estimatedDuration = roundVideoDuration(record.getCreativeType(), videoStreamInfo.getDuration());
        }

        estimatedDuration =
                Math.min(limits.getDurationCaptureStop(), Math.max(limits.getDurationMin(), estimatedDuration));
        return estimatedDuration;
    }

    @Override
    public void afterInsert(VideoFiles record) {
        if (record.getStatus() == FileStatus.READY || record.getStatus() == FileStatus.CONVERTING
                || VideoCreativeType.isVideoConstructor(record.getCreativeType())) {
            return;
        }

        record.setStrmPrefix("video_" + record.getId());

        var conversionParams = new ConvertionParams();
        Set<String> features = directService.getFeatures(record.getClientId(), null);
        var creativeType = record.getCreativeType();
        if (features.contains(CANVAS_CMS_ENCODE_FEATURE)
                && creativeType != VideoCreativeType.CPM_OUTDOOR//индор и аутдор поддержим в DIRECT-122634
                && creativeType != VideoCreativeType.CPM_INDOOR) {
            conversionParams.videoMetaId = startVhConverting(record);
        }
        if (conversionParams.videoMetaId != null) {
            conversionParams.status = FileStatus.CONVERTING;
        } else {//CMS заказать не получилось. Fallback на sandbox
            try {
                conversionParams.sandboxTaskId = startSandboxConverting(record, true);
                conversionParams.status = FileStatus.CONVERTING;
            } catch (RuntimeException e) {
                conversionParams.sandboxTaskId = null;
                conversionParams.status = FileStatus.ERROR;
                logger.info("SandBox failure: {}", e.getMessage());
            }
        }
        VideoFilesRepository.UpdateBuilder updateBuilder = new VideoFilesRepository.UpdateBuilder()
                .withConversionId(conversionParams.sandboxTaskId)
                .withVideoMetaId(conversionParams.videoMetaId)
                .withStatus(conversionParams.status)
                .withStrmPrefix(record.getStrmPrefix());

        videoFilesRepository.update(record.getId(), updateBuilder);

        if (conversionParams.status == FileStatus.ERROR) {
            throw new SandboxConvertionException("Failed to convert file", record.getId());
        }

        record.setConvertionTaskId(conversionParams.sandboxTaskId)
                .setStatus(conversionParams.status)
                .setVideoMetaId(conversionParams.videoMetaId);
    }

    public static class ConvertionParams {
        public Long sandboxTaskId;
        public FileStatus status;
        BigInteger videoMetaId;
    }

    private BigInteger startVhConverting(VideoFiles file) {
        try {
            return vhClient.startEncoding(file.getUrl()).getVideoMetaId();
        } catch (Exception e) {
            logger.error("CMS API create task error: " + e.getMessage(), e);
        }
        return null;
    }

    @Override
    VideoMetaData parseMetaData(StillageFileInfo info) {
        return stillageInfoConverter.toVideoMetaData(info);
    }

    @Override
    protected void validate(VideoMetaData videoMetaData, StillageFileInfo fileInfo,
                            VideoCreativeType videoCreativeType, Long presetId,
                            Long clientId) {
        Set<String> features = directService.getFeatures(clientId, null);
        VideoLimitsInterface limits = videoLimitsService.getLimits(videoCreativeType, features, presetId);
        Long clientConvertingFilesCount = clientConvertingFilesCount(clientId);
        MovieValidator movieValidator = new MovieValidator(videoMetaData, fileInfo, limits, videoCreativeType,
                features, videoPresetsService.getPresetSafe(presetId), videoGeometryService, clientConvertingFilesCount);
        movieValidator.validate();
    }

    private long clientConvertingFilesCount(Long clientId) {
        VideoFilesRepository.QueryBuilder convertingQuery = new VideoFilesRepository.QueryBuilder()
                .withStatus(FileStatus.CONVERTING)
                .withClientId(clientId);
        return videoFilesRepository.count(convertingQuery);
    }

    @Override
    public Long startSandboxConverting(VideoFiles file, boolean needFast) {
        var request = makeSandboxRequest(file, needFast);
        Long taskId = sandBoxService.createVideoConvertionTask(request);
        sandBoxService.startTask(taskId);
        return taskId;
    }

    public SandBoxService.SandboxTaskStatus getTaskStatus(VideoFiles file) {
        return sandBoxService.taskStatus(file.getConvertionTaskId());
    }


    @Override
    public void stopSandboxConverting(String fileId) {
        VideoFiles record = videoFilesRepository.findByIdAndQuery(fileId, new VideoFilesRepository.QueryBuilder());

        if (record.getStatus() != FileStatus.CONVERTING) {
            return;
        }

        try {
            sandBoxService.stopTask(record.getConvertionTaskId());
        } catch (Exception e) {
            logger.error("stopSandboxConverting failure: {}", e.getMessage());
        } finally {
            VideoFilesRepository.UpdateBuilder updateBuilder = new VideoFilesRepository.UpdateBuilder()
                    .withStatus(FileStatus.ERROR);

            videoFilesRepository.update(fileId, updateBuilder);
        }
    }

    @Override
    public FileStatus updateConvertingFile(String fileId, SandBoxService.SandboxConversionTaskOutput output)
            throws IOException {
        List<VideoFiles.VideoFormat> formats = getFormatsFromSandboxResult(output);
        VideoFiles.VideoThumbnail thumbnail = getThumbnailFromSandboxResult(output);

        if (formats == null && thumbnail == null) {
            return FileStatus.CONVERTING;
        }

        VideoFiles record = videoFilesRepository.findById(fileId);

        if (record == null) {
            logger.warn("File not found {}", fileId);
            throw new IOException("File not found");
        }

        VideoFilesRepository.UpdateBuilder updateBuilder = new VideoFilesRepository.UpdateBuilder();

        if (formats != null) {
            updateBuilder.withFormats(formats);
        }

        // обновляем thumbnail только тогда, когда его еще нет
        if (thumbnail != null && record.getThumbnail() == null) {
            updateBuilder.withThumbnail(thumbnail);
        }

        updateBuilder.withStatus(FileStatus.CONVERTING);

        VideoFilesRepository.QueryBuilder queryBuilder = new VideoFilesRepository.QueryBuilder()
                .withId(fileId)
                .withStockFileId(null);

        videoFilesRepository.update(queryBuilder, updateBuilder);

        record = videoFilesRepository.findById(fileId);

        if (record.getStatus() != FileStatus.READY && isCompleted(record)) {
            videoFilesRepository.update(fileId, new VideoFilesRepository.UpdateBuilder().withStatus(FileStatus.READY));
            logTranscodingDuration(record.getDate());
            return FileStatus.READY;
        }

        return record.getStatus();
    }

    private void logTranscodingDuration(Date dateFrom) {
        Duration executeDuration = Duration.ofMillis(System.currentTimeMillis() - dateFrom.getTime());
        logger.info("Video transcoding task duration {} sec", executeDuration.toSeconds());
    }

    private static List<VideoFiles.VideoFormat> getFormatsFromSandboxResult(
            SandBoxService.SandboxConversionTaskOutput output) {
        List<VideoFiles.VideoFormat> formats = null;

        if (output.getFormats() != null) {
            formats = new ArrayList<>();

            if (!Strings.isNullOrEmpty(output.getStreamUrl())) {

                VideoFiles.VideoFormat streamFormat = new VideoFiles.VideoFormat();
                String streamUrl = output.getStreamUrl();

                streamFormat.setId(Paths.get(streamUrl).getParent().toString());
                streamFormat.setMimeType("application/vnd.apple.mpegurl");
                streamFormat.setUrl(streamUrl);
                formats.add(streamFormat);
            }

            formats.addAll(output.getFormats());

            //XXX: maybe get rid of that delivery thing?
            formats.forEach(e -> e.setDelivery("progressive"));
        }

        return formats;
    }

    private VideoFiles.VideoThumbnail getThumbnailFromSandboxResult(SandBoxService.SandboxConversionTaskOutput output) {
        if (output != null && output.getThumbnailUrls() != null && !output.getThumbnailUrls().isEmpty()) {
            return getThumbnailFromUrl(output.getThumbnailUrls().get(0));
        }
        return null;
    }

    private VideoFiles.VideoThumbnail getThumbnailFromStillageResult(StillageFileInfo output) {
        if (output != null && output.getUrl() != null) {
            return getThumbnailFromUrl(output.getUrl());
        }
        return null;
    }

    @Override
    public VideoFiles.VideoThumbnail getThumbnailFromUrl(String url) {
        AvatarsPutCanvasResult result = avatarsService.upload(url);

        VideoFiles.VideoThumbnail thumbnail = new VideoFiles.VideoThumbnail();

        AvatarsPutCanvasResult.SizeInfo previewInfo = result.getSizes().getPreview480p();
        AvatarsPutCanvasResult.SizeInfo origInfo = result.getSizes().getOrig();

        VideoFiles.VideoThumbnail.ThumbnailPreview preview = new VideoFiles.VideoThumbnail.ThumbnailPreview()
                .setUrl(previewInfo.getUrl())
                .setWidth((long) previewInfo.getWidth())
                .setHeight((long) previewInfo.getHeight());

        thumbnail.setPreview(preview)
                .setUrl(origInfo.getUrl())
                .setWidth((long) origInfo.getWidth())
                .setHeight((long) origInfo.getHeight());

        return thumbnail;
    }

    /*
            Checks if the file has all conversion done -- has all formats and a thumbnail

            Since we allow video from up to 360p (see :py:module:`video_validation`)
            the minimal set of formats is 240p (fast and dirty) and 360p
            -- and 240p is always present if formats are present.

            Ergo, we need to check if there are 360p formats for all expected container types
            to be sure that conversion process ended successfully.
     */
    private boolean isCompleted(VideoFiles record) {
        if (record.getFormats() == null || record.getThumbnail() == null || record.getThumbnail().getUrl() == null) {
            return false;
        }

        VideoCreativeType type = guessCreativeType(record);

        final Set<String> expectedFormats;

        if (type == VideoCreativeType.CPM_OUTDOOR) {
            if (record.getRatio().equals("2:1")) {
                expectedFormats = Sets.newHashSet("360p.mp4");
            } else if (record.getRatio().equals("23:18")) {
                expectedFormats = Sets.newHashSet("576p.mp4");
            } else if (record.getRatio().equals("78:55")) {
                expectedFormats = Sets.newHashSet("880p.mp4");
            } else if (record.getRatio().equals("94:25")) {
                expectedFormats = Sets.newHashSet("400p.mp4");
            } else if (record.getRatio().equals("4:3")) {
                expectedFormats = Sets.newHashSet("1152p.mp4");
            } else if (record.getRatio().equals("10:3")) {
                expectedFormats = Sets.newHashSet("576p.mp4");
            } else {
                expectedFormats = Sets.newHashSet("416p.mp4");
            }
        } else if (type == VideoCreativeType.CPM_INDOOR) {

            if (record.getRatio().equals("9:16")) {
                expectedFormats = Sets.newHashSet("1920p.mp4");
            } else {
                expectedFormats = Sets.newHashSet("1080p.mp4");
            }
        } else if (type == VideoCreativeType.HTML5) {
            expectedFormats = Sets.newHashSet("240p.mp4");
        } else {
            expectedFormats = Sets.newHashSet("360p.mp4", "360p.webm");
        }

        if (directService.getFeatures(record.getClientId(), null).contains(CANVAS_VIDEO_ANY_SIZE_ALLOWED_FEATURE)) {
            expectedFormats.clear();
            expectedFormats.add("360p.mp4");
        }

        for (VideoFiles.VideoFormat format : record.getFormats()) {
            expectedFormats.removeIf(s -> format.getId().endsWith(s));
        }

        return expectedFormats.isEmpty();
    }

    private static VideoCreativeType guessCreativeType(VideoFiles record) {
        if (record.getCreativeType() != null) {
            return record.getCreativeType();
        }

        if (record.getRatio() != null && (Set.of("2:1", "3:1", "4:3", "23:18", "78:55", "94:25", "10:3")
                .contains(record.getRatio()))) {
            return VideoCreativeType.CPM_OUTDOOR;
        }

        return VideoCreativeType.TEXT;
    }


    @Override
    public CleanupOldConversionsResult cleanupOldConversions(Date from, Date to) {
        List<String> readyFileIds = new ArrayList<>();
        List<VideoFiles> failedTasks = findOldFailedFiles(from, to);
        int missedSandboxTasksCount = 0;

        for (VideoFiles file : failedTasks) {
            Long taskId = file.getConvertionTaskId();

            logger.info("cleanup: _id: {}, taskId={}, status={}", file.getId(), taskId, file.getStatus());

            if (taskId == null) {
                try {
                    logger.info("cleanup: restarting {}", taskId);
                    afterInsert(file);
                } catch (Exception e) {
                    logger.warn("cleanup fail", e);
                }
            } else {

                SandBoxService.SandboxTaskStatus status;

                try {
                    status = getTaskStatus(file);
                    logger.info("Task {} (file id={}) has status {} in sandbox", file.getConvertionTaskId(),
                            file.getId(), status);
                } catch (HttpClientErrorException e) {
                    logger.info("Task {} (file id={}) wasn't found in sandbox: {}", file.getConvertionTaskId(),
                            file.getId(), e.getMessage());

                    if (e.getRawStatusCode() == 404) {
                        missedSandboxTasksCount++;
                    }

                    continue;
                }

                /*
                 # HOTFIX, related tickets:
                 # https://st.yandex-team.ru/VH-3003
                 # https://st.yandex-team.ru/CANVAS-930
                 # https://st.yandex-team.ru/VH-3987
                 # ...and now we have quotas in sandbox :(
                 # TODO: mark files as failed to convert?
                 */
                if (status == SandBoxService.SandboxTaskStatus.FAILURE) {
                    logger.info("Task {} with status FAILURE, skipping", taskId);
                    continue;
                } else if (status == SandBoxService.SandboxTaskStatus.EXCEPTION) {
                    logger.info("Task {} with status EXCEPTION, skipping", taskId);
                    continue;
                } else if (status == SandBoxService.SandboxTaskStatus.TIMEOUT) {
                    logger.info("Task {} with status TIMEOUT, skipping", taskId);
                    continue;
                } else if (SandBoxService.SandboxTaskStatus.EXECUTE.contains(status)) {
                    logger.info("Task {} is still working, skipping", taskId);
                    continue;
                }

                try {
                    SandBoxService.SandboxConversionTaskOutput output = sandBoxService.taskOutput(taskId);
                    FileStatus result = updateConvertingFile(file.getId(), output);
                    logger.info("cleanup: updating {}, result={}", taskId, result);

                    if (result != FileStatus.READY) {
                        file.setStatus(FileStatus.NEW);
                        logger.info("cleanup: re-starting {}", taskId);
                        afterInsert(file);
                    } else {
                        readyFileIds.add(file.getId());
                    }

                } catch (Exception e) {
                    logger.error("cleanup file failed: " + taskId, e);
                }
            }
        }

        return new CleanupOldConversionsResult(missedSandboxTasksCount, readyFileIds);
    }

    List<VideoFiles> findOldFailedFiles(Date from, Date to) {
        VideoFilesRepository.QueryBuilder queryBuilder = new VideoFilesRepository.QueryBuilder().or(
                new VideoFilesRepository.QueryBuilder()
                        .withStatus(FileStatus.ERROR),

                new VideoFilesRepository.QueryBuilder()
                        .withNonNullConversionTaskId()
                        .withStatusNe(FileStatus.READY)
        );

        queryBuilder.withDateBetween(from, to);

        return videoFilesRepository.findByQuery(queryBuilder);
    }

    private static Number getFirstStreamWidth(StillageFileInfo fileInfo) {
        return (Number) extractFirstVideoStreamInfo(fileInfo).get("width");
    }

    private static Number getFirstStreamHeight(StillageFileInfo fileInfo) {
        return (Number) extractFirstVideoStreamInfo(fileInfo).get("height");
    }

    private static Map extractFirstVideoStreamInfo(StillageFileInfo fileInfo) {
        return (Map) ((List) fileInfo.getMetadataInfo().get("videoStreams")).get(0);
    }

}
