package ru.yandex.canvas.controllers.video;

import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import com.google.common.collect.ImmutableMap;
import io.swagger.annotations.ApiParam;
import org.hibernate.validator.constraints.URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.multipart.MultipartFile;

import ru.yandex.canvas.configs.auth.AuthorizeBy;
import ru.yandex.canvas.controllers.video.wrappers.VideoFileWrapper;
import ru.yandex.canvas.exceptions.BadRequestException;
import ru.yandex.canvas.exceptions.InternalServerError;
import ru.yandex.canvas.exceptions.ValidationErrorsException;
import ru.yandex.canvas.exceptions.VideoIntegrityValidationException;
import ru.yandex.canvas.model.ValidationError;
import ru.yandex.canvas.model.direct.Privileges;
import ru.yandex.canvas.model.stillage.StillageFileInfo;
import ru.yandex.canvas.model.stillage.StillageInfoConverter;
import ru.yandex.canvas.model.video.Addition;
import ru.yandex.canvas.model.video.CustomVastParams;
import ru.yandex.canvas.model.video.addition.AdditionData;
import ru.yandex.canvas.model.video.addition.AdditionDataBundle;
import ru.yandex.canvas.model.video.addition.AdditionElement;
import ru.yandex.canvas.model.video.addition.options.AdditionElementOptions;
import ru.yandex.canvas.model.video.addition.options.AgeElementOptions;
import ru.yandex.canvas.model.video.addition.options.BodyElementOptions;
import ru.yandex.canvas.model.video.addition.options.ButtonElementOptions;
import ru.yandex.canvas.model.video.addition.options.DisclaimerElementOptions;
import ru.yandex.canvas.model.video.addition.options.DomainElementOptions;
import ru.yandex.canvas.model.video.addition.options.LegalElementOptions;
import ru.yandex.canvas.model.video.addition.options.TitleElementOptions;
import ru.yandex.canvas.model.video.files.Movie;
import ru.yandex.canvas.service.AuthService;
import ru.yandex.canvas.service.DirectService;
import ru.yandex.canvas.service.SessionParams;
import ru.yandex.canvas.service.StillageService;
import ru.yandex.canvas.service.TankerKeySet;
import ru.yandex.canvas.service.video.HttpUtils;
import ru.yandex.canvas.service.video.MovieService;
import ru.yandex.canvas.service.video.MovieValidator;
import ru.yandex.canvas.service.video.Ratio;
import ru.yandex.canvas.service.video.VideoCreativeType;
import ru.yandex.canvas.service.video.VideoMetaData;
import ru.yandex.canvas.service.video.VideoPresetsService;

import static ru.yandex.canvas.VideoConstants.DEFAULT_VPAID_PCODE_URL;
import static ru.yandex.canvas.configs.auth.AuthorizeBy.AuthType.DIRECT_TOKEN;
import static ru.yandex.direct.utils.CommonUtils.nvl;


/**
 * Контроллер для быстрого создания креативов в ТГО.
 * Ручки загрузки файлов выбирают пресет в зависимости от отношения сторон видео.
 * Аналогично
 * <a href="https://a.yandex-team.ru/arc/trunk/arcadia/yabs/rmp/backend/src/stillage/client.py?rev=r8131750#L12-23">выбору пресета в uc/uac</a>
 */
@RestController
@RequestMapping("/video/default")
public class VideoDefaultController {

    private static final Logger logger = LoggerFactory.getLogger(VideoDefaultController.class);

    private static final String DEFAULT_BUNDLE_NAME = "video-banner_theme_empty";
    private static final String DEFAULT_ADDITION_NAME = "Новый видеокреатив";

    static final Long CPC_DEFAULT_WIDE_PRESET_ID = 5L;
    static final Long CPC_DEFAULT_SQUARE_PRESET_ID = 25L;
    static final Long CPC_DEFAULT_TALL_PRESET_ID = 28L;

    static final Long MOBILE_CONTENT_DEFAULT_WIDE_PRESET_ID = 15L;
    static final Long MOBILE_CONTENT_DEFAULT_SQUARE_PRESET_ID = 16L;
    static final Long MOBILE_CONTENT_DEFAULT_TALL_PRESET_ID = 17L;

    static final Long CPM_DEFAULT_WIDE_PRESET_ID = 6L;
    static final Long CPM_DEFAULT_SQUARE_PRESET_ID = 10L;
    static final Long CPM_DEFAULT_TALL_PRESET_ID = 9L;

    static final Long NON_SKIPPABLE_CPM_DEFAULT_WIDE_PRESET_ID = 7L;
    static final Long NON_SKIPPABLE_CPM_DEFAULT_SQUARE_PRESET_ID = 18L;
    static final Long NON_SKIPPABLE_CPM_DEFAULT_TALL_PRESET_ID = 19L;

    static final Long CPM_YNDX_FRONTPAGE_PRESET_ID = 406L;

    static final Set<VideoCreativeType> SUPPORTED_VIDEO_TYPES = Set.of(VideoCreativeType.MOBILE_CONTENT,
            VideoCreativeType.CPM, VideoCreativeType.TEXT, VideoCreativeType.NON_SKIPPABLE_CPM,
            VideoCreativeType.CPM_YNDX_FRONTPAGE
    );

    private static final Map<String, VideoCreativeType> VIDEO_CREATIVE_TYPE_BY_NAME =
            ImmutableMap.<String, VideoCreativeType>builder()
                    .put("text", VideoCreativeType.TEXT)
                    .put("cpm", VideoCreativeType.CPM)
                    .put("non_skippable_cpm", VideoCreativeType.NON_SKIPPABLE_CPM)
                    .put("mobile_content", VideoCreativeType.MOBILE_CONTENT)
                    .build();


    private final VideoAdditionCreatorService videoAdditionCreatorService;
    private final AuthService authService;
    private final MovieService movieService;
    private final DirectService directService;
    private final StillageService stillageService;
    private final StillageInfoConverter stillageInfoConverter;
    private final SessionParams sessionParams;
    private final VideoPresetsService videoPresetsService;

    public VideoDefaultController(
            VideoAdditionCreatorService videoAdditionCreatorService,
            AuthService authService,
            MovieService movieService,
            DirectService directService,
            StillageService stillageService,
            StillageInfoConverter stillageInfoConverter,
            SessionParams sessionParams,
            VideoPresetsService videoPresetsService) {
        this.videoAdditionCreatorService = videoAdditionCreatorService;
        this.authService = authService;
        this.movieService = movieService;
        this.directService = directService;
        this.stillageService = stillageService;
        this.stillageInfoConverter = stillageInfoConverter;
        this.sessionParams = sessionParams;
        this.videoPresetsService = videoPresetsService;
    }

    public static class UploadUrlRequest {
        @NotNull
        @URL
        private String url;

        public String getUrl() {
            return url;
        }

        public UploadUrlRequest setUrl(String url) {
            this.url = url;
            return this;
        }
    }

    @PostMapping("")
    public ResponseEntity<?> createDefaultAdditionFromFile(
            @RequestParam("file") final MultipartFile file,
            @RequestParam("client_id") Long clientId
    ) {
        authService.requirePermission(Privileges.Permission.CREATIVE_CREATE);
        VideoFileWrapper fileWrapper = upload(file, clientId, null);
        return ResponseEntity.status(HttpStatus.CREATED).body(fileWrapper);
    }

    @PostMapping("/url")
    public ResponseEntity<?> createDefaultAdditionFromUrl(
            @RequestBody @Valid UploadUrlRequest url,
            BindingResult bindingResult,
            @RequestParam(value = "filename", required = false) String filename,
            @RequestParam("client_id") Long clientId
    ) {
        authService.requirePermission(Privileges.Permission.CREATIVE_CREATE);

        if (bindingResult.hasErrors()) {
            logger.warn(bindingResult.getAllErrors().toString());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ValidationError(
                    TankerKeySet.VIDEO_VALIDATION_MESSAGES.interpolate("not_an_url"),
                    bindingResult));
        }
        if (url.getUrl().isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ValidationError(
                    TankerKeySet.VIDEO_VALIDATION_MESSAGES.interpolate("empty_url"),
                    bindingResult));
        }

        URI uri = HttpUtils.makeURI(url.getUrl());
        // если название для файла не передано, то генерируем из url
        if (filename == null || filename.isEmpty()) {
            filename = HttpUtils.makeFilenameFromUrl(uri);
        }

        VideoFileWrapper fileWrapper = uploadByUrl(uri, filename, clientId, null);
        return ResponseEntity.status(HttpStatus.CREATED).body(fileWrapper);
    }

    @PostMapping("/create")
    public ResponseEntity<?> createDefaultAddition(
            @RequestParam("client_id") Long clientId,
            @RequestParam("preset_id") Long presetId,
            @RequestParam("video_id") String videoId
    ) {
        authService.requirePermission(Privileges.Permission.CREATIVE_CREATE);
        VideoCreativeType creativeType = getVideoCreativeType();
        Addition addition;

        if (creativeType == VideoCreativeType.CPM || creativeType == VideoCreativeType.NON_SKIPPABLE_CPM) {
            addition = cpmAddition(presetId, videoId);
        } else {
            addition = defaultAddition(presetId, videoId);
        }
        videoAdditionCreatorService.createAddition(authService.getUserId(), clientId, addition,
                new CustomVastParams().setVpaidPcodeUrl(DEFAULT_VPAID_PCODE_URL));

        return ResponseEntity.ok(addition);
    }

    private VideoFileWrapper upload(MultipartFile file, Long clientId, @Nullable VideoCreativeType videoCreativeType) {
        try {
            VideoCreativeType creativeType = nvl(videoCreativeType, getVideoCreativeType());
            String filename = file.getOriginalFilename();
            StillageFileInfo info = stillageService.uploadFile(filename, file.getBytes());
            Long presetId = getPresetId(info, creativeType);
            Movie movie = movieService.upload(info, filename, clientId, creativeType, presetId);
            Set<String> features = directService.getFeatures(clientId, null);
            return new VideoFileWrapper(movie, presetId, creativeType, features);
        } catch (HttpStatusCodeException e) {
            var exceptionText = e.getResponseBodyAsString();
            logger.error("File uploading to stillage failed: {}, {}", e.getStatusCode(), exceptionText);
            if (exceptionText.contains("Video integrity validation failed")) {
                // DIRECT-123887
                throw new VideoIntegrityValidationException(
                        TankerKeySet.VIDEO_VALIDATION_MESSAGES.key("video-integrity-validation-failed"), e);
            }
            throw new InternalServerError(e);
        } catch (IOException e) {
            throw new InternalServerError(e);
        }
    }

    private VideoFileWrapper uploadByUrl(URI uri, String filename, Long clientId,
                                         @Nullable VideoCreativeType videoCreativeType) {
        try {
            VideoCreativeType creativeType = nvl(videoCreativeType, getVideoCreativeType());
            StillageFileInfo info = stillageService.uploadFile(filename, uri.toURL());
            Long presetId = getPresetId(info, creativeType);
            Movie movie = movieService.upload(info, filename, clientId, creativeType, presetId);
            Set<String> features = directService.getFeatures(clientId, null);
            return new VideoFileWrapper(movie, presetId, creativeType, features);
        } catch (HttpStatusCodeException e) {
            logger.error(String.format("File uploading to stillage failed: %s, %s",
                    e.getStatusCode(), e.getResponseBodyAsString()), e);
            if (e.getStatusCode() == HttpStatus.PAYLOAD_TOO_LARGE) {
                throw new BadRequestException("Payload too large");
            } else if (e.getStatusCode().is4xxClientError() || e.getStatusCode().is5xxServerError()) {
                throw new BadRequestException("Provided file url is unavailable");
            } else {
                throw new InternalServerError(e);
            }
        } catch (IOException e) {
            throw new InternalServerError(e);
        }
    }

    /**
     * Возвращает тип видеокреатива, выбираемый с помощью session_data
     */
    private VideoCreativeType getVideoCreativeType() {
        VideoCreativeType sessionCreativeType = sessionParams.getCreativeType();

        if (sessionCreativeType != null && SUPPORTED_VIDEO_TYPES.contains(sessionCreativeType)) {
            return sessionCreativeType;
        } else {
            // Чтобы не усложнять считаем text дефолтом если с фронта пришло что-то странное
            return VideoCreativeType.TEXT;
        }
    }

    private Long getPresetId(StillageFileInfo info, VideoCreativeType creativeType) {
        VideoMetaData videoMetaData = stillageInfoConverter.toVideoMetaData(info);

        if (videoMetaData.getVideoStreams() != null && !videoMetaData.getVideoStreams().isEmpty()) {
            VideoMetaData.VideoStreamInfo videoStream = videoMetaData.getVideoStreams().get(0);
            Ratio ratio = new Ratio(videoStream.getWidth(), videoStream.getHeight());

            switch (creativeType) {
                case MOBILE_CONTENT:
                    if (ratio.isWideVideo()) {
                        return MOBILE_CONTENT_DEFAULT_WIDE_PRESET_ID;
                    } else if (ratio.isTallVideo()) {
                        return MOBILE_CONTENT_DEFAULT_TALL_PRESET_ID;
                    } else {
                        return MOBILE_CONTENT_DEFAULT_SQUARE_PRESET_ID;
                    }
                case CPM:
                    if (ratio.isWideVideo()) {
                        return CPM_DEFAULT_WIDE_PRESET_ID;
                    } else if (ratio.isTallVideo()) {
                        return CPM_DEFAULT_TALL_PRESET_ID;
                    } else {
                        return CPM_DEFAULT_SQUARE_PRESET_ID;
                    }
                case NON_SKIPPABLE_CPM:
                    if (ratio.isWideVideo()) {
                        return NON_SKIPPABLE_CPM_DEFAULT_WIDE_PRESET_ID;
                    } else if (ratio.isTallVideo()) {
                        return NON_SKIPPABLE_CPM_DEFAULT_TALL_PRESET_ID;
                    } else {
                        return NON_SKIPPABLE_CPM_DEFAULT_SQUARE_PRESET_ID;
                    }
                case CPM_YNDX_FRONTPAGE:
                    return CPM_YNDX_FRONTPAGE_PRESET_ID;
                default:
                    if (ratio.isWideVideo()) {
                        return CPC_DEFAULT_WIDE_PRESET_ID;
                    } else if (ratio.isTallVideo()) {
                        return CPC_DEFAULT_TALL_PRESET_ID;
                    } else {
                        return CPC_DEFAULT_SQUARE_PRESET_ID;
                    }
            }
        }

        throw new ValidationErrorsException(List.of(TankerKeySet.VIDEO_VALIDATION_MESSAGES
                .interpolate(MovieValidator.INVALID_VIDEO_FILE_FORMAT)));
    }

    private static Addition cpmAddition(Long presetId, String videoId) {
        return new Addition()
                .setPresetId(presetId)
                .setName(DEFAULT_ADDITION_NAME)
                .setData(new AdditionData()
                        .setBundle(new AdditionDataBundle().setName(DEFAULT_BUNDLE_NAME))
                        .setElements(List.of(
                                new AdditionElement(AdditionElement.ElementType.ADDITION,
                                        new AdditionElementOptions()
                                                .setVideoId(videoId),
                                        true
                                ),
                                new AdditionElement(AdditionElement.ElementType.BUTTON,
                                        new ButtonElementOptions()
                                                .setColor("#FFDC00")
                                                .setPosition("left-bottom")
                                                .setTextColor("#000000"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.AGE,
                                        new AgeElementOptions()
                                                .setText("18")
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.LEGAL,
                                        new LegalElementOptions()
                                                .setText("")
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        false)
                        )));

    }

    private static Addition defaultAddition(Long presetId, String videoId) {
        return new Addition()
                .setPresetId(presetId)
                .setName(DEFAULT_ADDITION_NAME)
                .setData(new AdditionData()
                        .setBundle(new AdditionDataBundle().setName(DEFAULT_BUNDLE_NAME))
                        .setElements(List.of(
                                new AdditionElement(AdditionElement.ElementType.ADDITION,
                                        new AdditionElementOptions()
                                                .setVideoId(videoId),
                                        true
                                ),
                                new AdditionElement(AdditionElement.ElementType.TITLE,
                                        new TitleElementOptions()
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.BODY,
                                        new BodyElementOptions()
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.DOMAIN,
                                        new DomainElementOptions(),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.BUTTON,
                                        new ButtonElementOptions()
                                                .setColor("#FFDC00")
                                                .setTextColor("#000000"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.DISCLAIMER,
                                        new DisclaimerElementOptions()
                                                .setText("")
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.AGE,
                                        new AgeElementOptions()
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        true),
                                new AdditionElement(AdditionElement.ElementType.LEGAL,
                                        new LegalElementOptions()
                                                .setText("")
                                                .setBackgroundColor("#000000")
                                                .setTextColor("#ffffff"),
                                        false)
                        )));
    }

    private static VideoCreativeType toVideoCreativeType(@Nullable String creativeType) {
        if (creativeType == null) {
            return VideoCreativeType.TEXT;
        }
        return VIDEO_CREATIVE_TYPE_BY_NAME.getOrDefault(creativeType, VideoCreativeType.TEXT);
    }


    @AuthorizeBy({DIRECT_TOKEN})
    @PostMapping("/create-video")
    public ResponseEntity<?> createVideoFromUrl(
            @RequestBody @Valid VideoDefaultController.UploadUrlRequest url,
            BindingResult bindingResult,
            @RequestParam("client_id") Long clientId,
            @ApiParam(value = "creative_type", required = false) @RequestParam @Nullable String creativeType
    ) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ValidationError(
                    TankerKeySet.VIDEO_VALIDATION_MESSAGES.interpolate("not_an_url"),
                    bindingResult));
        }
        if (url.getUrl().isEmpty()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ValidationError(
                    TankerKeySet.VIDEO_VALIDATION_MESSAGES.interpolate("empty_url"),
                    bindingResult));
        }

        URI uri = HttpUtils.makeURI(url.getUrl());
        String filename = HttpUtils.makeFilenameFromUrl(uri);

        VideoFileWrapper fileWrapper = uploadByUrl(uri, filename, clientId, toVideoCreativeType(creativeType));
        //ResponseEntity.status(HttpStatus.CREATED) вернуть не могу, CanvasClient считает за ошибку
        return ResponseEntity.ok(fileWrapper);
    }

    @AuthorizeBy({DIRECT_TOKEN})
    @PostMapping("/create-video-from-file")
    public ResponseEntity<VideoFileWrapper> createVideoFromFile(
            @RequestParam("file") final MultipartFile file,
            @RequestParam("client_id") Long clientId,
            @ApiParam(value = "creative_type", required = false) @RequestParam @Nullable String creativeType
    ) {
        VideoFileWrapper fileWrapper = upload(file, clientId, toVideoCreativeType(creativeType));
        return ResponseEntity.ok(fileWrapper);
    }

    @AuthorizeBy({DIRECT_TOKEN})
    @PostMapping("/get-video")
    public ResponseEntity<?> getVideoStatus(
            @RequestParam("client_id") Long clientId,
            @RequestParam("preset_id") Long presetId,
            @RequestParam("video_id") String videoId
    ) {
        Movie movie = movieService.lookupMovie(videoId, null, clientId, presetId);
        return ResponseEntity.ok(movie);
    }

    @AuthorizeBy({DIRECT_TOKEN})
    @PostMapping("/get-created-video")
    public ResponseEntity<?> getCreatedVideo(
            @RequestParam("client_id") Long clientId,
            @RequestParam("preset_id") Long presetId,
            @RequestParam("video_id") String videoId,
            @ApiParam(value = "creative_type") @RequestParam @Nullable String creativeType
    ) {
        Movie movie = movieService.lookupMovie(videoId, null, clientId, presetId);
        Set<String> features = directService.getFeatures(clientId, null);
        VideoFileWrapper fileWrapper = new VideoFileWrapper(movie, presetId, toVideoCreativeType(creativeType), features);
        return ResponseEntity.ok(fileWrapper);
    }

    @AuthorizeBy({DIRECT_TOKEN})
    @PostMapping("/create-addition")
    public ResponseEntity<?> createDefaultAdditionByApi(
            @RequestParam("client_id") Long clientId,
            @RequestParam("preset_id") Long presetId,
            @RequestParam("video_id") String videoId
    ) {
        var videoCreativeType = videoPresetsService.fromPresetId(presetId);

        Addition addition;

        if (videoCreativeType == VideoCreativeType.CPM || videoCreativeType == VideoCreativeType.NON_SKIPPABLE_CPM) {
            addition = cpmAddition(presetId, videoId);
        } else {
            addition = defaultAddition(presetId, videoId);
        }

        //userId не нужен, в DisplayCanvasController.uploadCreatives никак не используется
        videoAdditionCreatorService.createAddition(null, clientId, addition,
                new CustomVastParams().setVpaidPcodeUrl(DEFAULT_VPAID_PCODE_URL));

        return ResponseEntity.ok(addition);
    }
}
