package ru.yandex.canvas.service.video;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.annotation.Nullable;

import com.google.gson.Gson;

import ru.yandex.canvas.exceptions.BadRequestException;
import ru.yandex.canvas.model.video.files.FileStatus;
import ru.yandex.canvas.model.video.vc.feed.VideoConstructorFeed;
import ru.yandex.canvas.model.video.vc.feed.VideoConstructorFeedCropParams;
import ru.yandex.canvas.model.video.vc.feed.VideoConstructorFeedParsed;
import ru.yandex.canvas.model.video.vc.files.VideoConstructorFile;
import ru.yandex.canvas.model.video.vc.files.VideoConstructorFiles;
import ru.yandex.canvas.repository.video.VideoConstructorFeedsRepository;
import ru.yandex.canvas.repository.video.VideoConstructorFilesRepository;
import ru.yandex.canvas.service.DirectService;
import ru.yandex.canvas.service.FileValidator;
import ru.yandex.canvas.service.SandBoxService;
import ru.yandex.canvas.service.TankerKeySet;
import ru.yandex.canvas.service.VideoLimitsInterface;
import ru.yandex.canvas.service.video.presets.VideoPreset;

import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.ID_TO_VIDEO_DATA;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.URL;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_AUDIO_URL;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_FEED_ARCHIVE_NAME;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_FEED_AUDIO_URLS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_FEED_ROW_NUMBERS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_FPS;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_HEIGHT;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_NAME;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_TEMPLATE_ID;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_WEB_HOOK_URL;
import static ru.yandex.canvas.service.SandBoxService.VideoTaskExtraFields.VIDEO_CONSTRUCTOR_WIDTH;

public class VideoConstructorFilesService {
    private static final String SANDBOX_OWNER = "DIRECT-VIDEO";

    private final VideoConstructorFilesRepository videoConstructorFilesRepository;
    private final SandBoxService sandBoxService;
    private final VideoLimitsService videoLimitsService;
    private final DirectService directService;
    private final VideoConstructorFeedsRepository videoConstructorFeedsRepository;
    private final VideoConstructorFeedsService videoConstructorFeedsService;
    private final String hookSecret;
    private final String canvasVideoApiBaseUrl;
    private final TankerKeySet videoConstructorTranslations;
    private final VideoGeometryService videoGeometryService;


    public VideoConstructorFilesService(VideoConstructorFilesRepository videoConstructorFilesRepository,
                                        SandBoxService sandBoxService,
                                        VideoLimitsService videoLimitsService,
                                        DirectService directService,
                                        VideoConstructorFeedsRepository videoConstructorFeedsRepository,
                                        VideoConstructorFeedsService videoConstructorFeedsService,
                                        String hookSecret,
                                        String canvasVideoApiBaseUrl, VideoGeometryService videoGeometryService) {
        this.videoConstructorFilesRepository = videoConstructorFilesRepository;
        this.sandBoxService = sandBoxService;
        this.videoLimitsService = videoLimitsService;
        this.directService = directService;
        this.videoConstructorFeedsRepository = videoConstructorFeedsRepository;
        this.videoConstructorFeedsService = videoConstructorFeedsService;
        this.hookSecret = hookSecret;
        this.canvasVideoApiBaseUrl = canvasVideoApiBaseUrl;
        this.videoGeometryService = videoGeometryService;
        this.videoConstructorTranslations = TankerKeySet.VIDEO_CONSTRUCTOR_TRANSLATIONS;
    }

    public VideoConstructorFile upload(VideoConstructorFile record,
                                       @Nullable List<VideoConstructorFeedCropParams> cropParams) {
        validateVideoConstructorFile(record);

        record.withStatus(FileStatus.CONVERTING);

        // делаем кроп изображений здесь, пока это самый удобный вариант, возможно надо будет делать асинхронно
        if (record.getFeedId() != null && cropParams != null) {
            VideoConstructorFeed croppedFeed = videoConstructorFeedsService.createFeedCropped(record.getFeedId(),
                    cropParams, record.getClientId());
            record.withFeedId(croppedFeed.getId());
        }

        VideoConstructorFile updatedRecord = videoConstructorFilesRepository.upsert(record);

        startConverting(updatedRecord);

        return updatedRecord;
    }

    Long startConverting(VideoConstructorFile file) {
        SandBoxService.SandboxCreateRequest request = makeSandboxRequest(file);
        Long taskId = sandBoxService.createVideoConvertionTask(request);
        sandBoxService.startTask(taskId);
        return taskId;
    }

    private SandBoxService.SandboxCreateRequest makeSandboxRequest(VideoConstructorFile file) {
        if (file.getId() == null) {
            throw new IllegalArgumentException(
                    "file must be already inserted before ffConvertTask is called");
        }

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

        Long scale = Optional.ofNullable(file.getScale()).orElse(1L);
        request.setPriority("SERVICE", "HIGH")
                .addCustomField(URL, file.getUrl() + "?client_id=" + file.getClientId() +
                        "&user_id=" + file.getUserId() + "&id=" + file.getId() + "&scale=" + scale)
                .addCustomField(VIDEO_CONSTRUCTOR_WIDTH, String.valueOf(file.getWidth() * scale))
                .addCustomField(VIDEO_CONSTRUCTOR_HEIGHT, String.valueOf(file.getHeight() * scale))
                .addCustomField(VIDEO_CONSTRUCTOR_WEB_HOOK_URL, makeHookUrl(file.getId()))
                .addCustomField(VIDEO_CONSTRUCTOR_NAME,
                        videoConstructorTranslations.interpolate("video-constructor-video-name", file.getName(),
                                formatDateForName(file.getCreationTime())))
                .addCustomField(VIDEO_CONSTRUCTOR_TEMPLATE_ID,
                        (String) file.getParams().getOrDefault("templateId", null));
        if (file.getAudioUrl() != null) {
            request.addCustomField(VIDEO_CONSTRUCTOR_AUDIO_URL, file.getAudioUrl());
        }
        if (file.getFps() != null) {
            request.addCustomField(VIDEO_CONSTRUCTOR_FPS, String.valueOf(file.getFps()));
        }

        request.setOwner(SANDBOX_OWNER);
        request.setType(SandboxTask.MAKE_VIDEO.getTaskName());

        // если это фид, то запускаем для всех его строк свою таску
        if (file.getFeedId() != null) {
            VideoConstructorFeed feed = videoConstructorFeedsRepository.findById(file.getFeedId(), file.getClientId());
            request.addCustomField(VIDEO_CONSTRUCTOR_FEED_ROW_NUMBERS, "0-" + (feed.getRowsCount() - 1));
            request.addCustomField(VIDEO_CONSTRUCTOR_FEED_ARCHIVE_NAME,
                    videoConstructorTranslations.interpolate("video-constructor-archive-name", file.getName(),
                            formatDateForName(file.getCreationTime())));

            // используем аудио из фида
            if (file.getFeedAudioFieldName() != null) {
                VideoConstructorFeedParsed feedParsed = videoConstructorFeedsService.getParsedFeed(feed);
                if (feedParsed.isFieldExists(file.getFeedAudioFieldName())) {
                    List<String> audioUrls = feedParsed.getFieldValues(file.getFeedAudioFieldName());
                    Map<Integer, String> rowNum2audioUrl = IntStream
                            .range(0, feedParsed.getRowsCount())
                            .boxed()
                            .collect(Collectors.toMap(Function.identity(), audioUrls::get));
                    request.addCustomField(VIDEO_CONSTRUCTOR_FEED_AUDIO_URLS, new Gson().toJson(rowNum2audioUrl));
                }
            }

            request.setType(SandboxTask.MAKE_FEED_VIDEOS.getTaskName());
        }

        return request;
    }

    public Long startRebuild(List<VideoConstructorFile> files, SandboxTask rebuildTask) {
        SandBoxService.SandboxCreateRequest request = makeSandboxRequestRebuild(files, rebuildTask);
        Long taskId = sandBoxService.createVideoConvertionTask(request);
        sandBoxService.startTask(taskId);
        return taskId;
    }

    private SandBoxService.SandboxCreateRequest makeSandboxRequestRebuild(List<VideoConstructorFile> files,
                                                                          SandboxTask rebuildTask) {
        Map<String, VideoDataForSandboxRequest> id2videoData = files.stream()
                .filter(file -> file.getId() != null && file.getMp4Url() != null)
                .collect(Collectors.toMap(
                        VideoConstructorFile::getId,
                        file -> new VideoDataForSandboxRequest(file.getMp4Url(),
                                (String) file.getParams().getOrDefault("templateId", null)),
                        (value1, value2) -> value1));

        if (id2videoData.isEmpty()) {
            throw new BadRequestException("rebuilding expects at least one file with not null id and mp4url");
        }

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

        request.setPriority("SERVICE", "HIGH")
                .addCustomField(ID_TO_VIDEO_DATA, new Gson().toJson(id2videoData))
                .addCustomField(VIDEO_CONSTRUCTOR_WEB_HOOK_URL, makeHookUrlUpdate());

        request.setOwner(SANDBOX_OWNER);
        request.setType(rebuildTask.getTaskName());

        return request;
    }

    private String makeHookUrl(String id) {
        return String.format("%s/constructor-files/%s/sandbox-hook?secret=%s", canvasVideoApiBaseUrl, id, hookSecret);
    }

    private String makeHookUrlUpdate() {
        return String.format("%s/constructor-files/sandbox-hook-update?secret=%s", canvasVideoApiBaseUrl, hookSecret);
    }

    private String formatDateForName(Date date) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
        return dateFormat.format(date);
    }

    /*
    Добавляем ссылки на видео, которые мы отсняли, в файл с фидом в виде отдельной колонки и загружаем его в mds
     */
    private String createFeedWithResultUrls(VideoConstructorFile file, List<String> resultUrls) {
        VideoConstructorFeedParsed feedParsed = videoConstructorFeedsService.getParsedFeedById(file.getFeedId());

        feedParsed.addNewField(videoConstructorTranslations.interpolate("video-constructor-result-feed-field-name"),
                resultUrls);
        return videoConstructorFeedsService.uploadFeed(
                VideoConstructorFeedsService.getResultFeedNameForMds(),
                videoConstructorTranslations.interpolate("video-constructor-result-feed-name",
                        file.getName(), formatDateForName(file.getCreationTime())) + ".csv",
                feedParsed);
    }

    /**
     * по вебхуку меняет url файла и статус
     */
    public void updateConvertingFile(String fileId, SandBoxService.SandboxVideoConstructorTaskOutput output) {
        String feedResultUrl = null;
        FileStatus fileStatus;
        // если завершилась съемка фида, то добавляем ссылки в файл с офферами
        if (output.getFeedMp4Urls() != null) {
            long readyVideosCount = output.getFeedMp4Urls().stream().filter(Objects::nonNull).count();

            if (!output.getSuccess() || readyVideosCount == 0) {
                fileStatus = FileStatus.ERROR;
            } else {
                VideoConstructorFile file = videoConstructorFilesRepository.findByIdInternal(fileId);
                VideoConstructorFeed videoConstructorFeed = videoConstructorFeedsRepository.findByIdInternal(
                        file.getFeedId());

                if (readyVideosCount == videoConstructorFeed.getRowsCount()) {
                    fileStatus = FileStatus.READY;
                } else {
                    fileStatus = FileStatus.SEMI_READY;
                }

                feedResultUrl = createFeedWithResultUrls(file, output.getFeedMp4Urls());
            }
        } else {
            fileStatus = output.getSuccess() ? FileStatus.READY : FileStatus.ERROR;
        }

        videoConstructorFilesRepository.updateFileFromSandboxTaskResult(fileId, output, fileStatus, feedResultUrl);
    }

    /**
     * массовое обновление файлов
     */
    public void updateFiles(List<SandBoxService.SandboxVideoConstructorTaskOutput> outputs) {
        outputs.forEach(output -> videoConstructorFilesRepository.updateFileFromSandboxTaskResult(output.getId(),
                output, null, null));
    }

    public VideoConstructorFiles find(final long clientId, final String name, final boolean descOrder,
                                      final int offset, final int limit, List<String> ratios,
                                      Double durationFrom, Double durationTo, Boolean hasAudio,
                                      FileStatus fileStatus, Boolean hasFeed, VideoPreset preset) {
        Map<String, List<Double>> ratioDurations = null;
        Double ratioDurationDelta = null;
        Ratio ratioFrom = null;
        Ratio ratioTo = null;

        if (preset != null) {
            VideoCreativeType creativeType = preset.videoCreativeType();
            VideoLimitsInterface limits = videoLimitsService.getLimits(creativeType, preset.getId());
            hasAudio = limits.getAudioRequired() ? null : false;
            ratios = limits.getAllowedVideoHwRatio();

            Set<String> features = directService.getFeatures(clientId, null);
            if (videoGeometryService.hasAllowedRatiosInterval(preset.getDescription().getGeometry(), features,
                    creativeType)) {
                var ratiosInterval = videoGeometryService.getRatiosByPreset(preset.getDescription().getGeometry(),
                        features, creativeType);
                ratioFrom = ratiosInterval.getFrom();
                ratioTo = ratiosInterval.getTo();
                ratios = null;
            }

            durationFrom = limits.getDurationMin() - limits.getDurationDelta();
            durationTo = limits.getDurationMax() + limits.getDurationDelta();
            ratioDurations = limits.getDurationLimitsByRatio().entrySet()
                    .stream()
                    .collect(Collectors.toMap(Map.Entry::getKey,
                            e -> ((List<Number>) ((Map) e.getValue()).get("durations")).stream()
                                    .map(Number::doubleValue)
                                    .collect(Collectors.toList())));
            ratioDurationDelta = limits.getDurationDelta();
            hasFeed = false;
        }

        return videoConstructorFilesRepository.find(clientId, name, descOrder, offset, limit, ratios, ratioFrom,
                ratioTo, durationFrom, durationTo, hasAudio, fileStatus, ratioDurations, ratioDurationDelta, hasFeed);
    }

    public VideoConstructorFile getById(final String id, final long clientId) {
        return videoConstructorFilesRepository.findById(id, clientId);
    }

    public List<VideoConstructorFile> getByIdsInternal(final List<String> ids) {
        return videoConstructorFilesRepository.findByIdsInternal(ids);
    }

    public boolean delete(String id, long clientId) {
        return videoConstructorFilesRepository.deleteFile(id, clientId);
    }

    private void validateVideoConstructorFile(VideoConstructorFile file) {
        FileValidator validator = new VideoConstructorFileValidator(videoConstructorFeedsRepository,
                videoConstructorFilesRepository, file);
        validator.validate();
    }

    public enum SandboxTask {
        MAKE_VIDEO("CANVAS_MAKE_VIDEO_IN_BROWSER"),
        REBUILD_PACKSHOT("CANVAS_VIDEO_REBUILD_PACKSHOT"),
        REBUILD_PREVIEW("CANVAS_VIDEO_REBUILD_PREVIEW"),
        MAKE_FEED_VIDEOS("CANVAS_MAKE_FEED_VIDEOS");

        private String taskName;

        SandboxTask(String taskName) {
            this.taskName = taskName;
        }

        public String getTaskName() {
            return taskName;
        }
    }

    public static class VideoDataForSandboxRequest {
        private final String mp4Url;
        private final String templateId;

        public VideoDataForSandboxRequest(String mp4Url, String templateId) {
            this.mp4Url = mp4Url;
            this.templateId = templateId;
        }

        public String getMp4Url() {
            return mp4Url;
        }

        public String getTemplateId() {
            return templateId;
        }
    }
}
