package ru.yandex.canvas.service;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.base.Stopwatch;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.request.body.multipart.ByteArrayPart;
import org.asynchttpclient.request.body.multipart.PartBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.retry.annotation.Retryable;

import ru.yandex.canvas.exceptions.AvatarsApiException;
import ru.yandex.canvas.model.avatars.AvatarsPutCanvasResult;
import ru.yandex.canvas.model.avatars.AvatarsPutResult;
import ru.yandex.canvas.model.avatars.AvatarsPutTurboResult;
import ru.yandex.direct.asynchttp.FetcherSettings;
import ru.yandex.direct.asynchttp.ParallelFetcher;
import ru.yandex.direct.asynchttp.ParallelFetcherFactory;
import ru.yandex.direct.asynchttp.ParsableBytesRequest;
import ru.yandex.direct.asynchttp.Result;
import ru.yandex.direct.http.smart.annotations.Json;
import ru.yandex.direct.http.smart.core.Call;
import ru.yandex.direct.http.smart.core.Smart;
import ru.yandex.direct.http.smart.http.GET;
import ru.yandex.direct.http.smart.http.Headers;
import ru.yandex.direct.http.smart.http.Multipart;
import ru.yandex.direct.http.smart.http.POST;
import ru.yandex.direct.http.smart.http.Part;
import ru.yandex.direct.http.smart.http.Path;
import ru.yandex.direct.solomon.SolomonUtils;
import ru.yandex.direct.tracing.Trace;
import ru.yandex.direct.tracing.TraceProfile;
import ru.yandex.direct.tvm.TvmIntegration;
import ru.yandex.direct.tvm.TvmService;
import ru.yandex.direct.utils.InterruptedRuntimeException;
import ru.yandex.direct.utils.JsonUtils;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static ru.yandex.direct.http.smart.error.ErrorUtils.checkResultForErrors;
import static ru.yandex.direct.utils.HashingUtils.getMd5HashAsHexString;

/**
 * @author skirsanov
 */
@ParametersAreNonnullByDefault
public class AvatarsService {
    public static final String TURBO_NAMESPACE = "turbo";
    private static final Logger logger = LoggerFactory.getLogger(AvatarsService.class);
    private static final Marker PERFORMANCE = MarkerFactory.getMarker("PERFORMANCE");
    /**
     * @see <a href="https://st.yandex-team.ru/MDS-2840">https://st.yandex-team.ru/MDS-2840</a>
     */

    private final String readServiceUri;
    private final String readServiceHost;
    private final String avatarsNamespace;
    private final Pattern avatarsReadPattern;

    private final ParallelFetcherFactory parallelFetcherFactory;

    private final Api api;

    public AvatarsService(String writeServiceUri,
                          String readServiceUri,
                          String avatarsNamespace,
                          TvmIntegration tvmIntegration,
                          TvmService tvmService,
                          AsyncHttpClient asyncHttpClient) {
        this.readServiceUri = readServiceUri;
        this.avatarsNamespace = avatarsNamespace;

        try {
            this.readServiceHost = (new URI(readServiceUri)).getHost();
        } catch (URISyntaxException e) {
            logger.error("invalid readServiceHost", e);
            throw new RuntimeException(e);
        }

        this.avatarsReadPattern = Pattern.compile(this.readServiceUri + "/get-" + this.avatarsNamespace +
                "/(\\d+)/(\\w+)/orig");

        this.parallelFetcherFactory = new ParallelFetcherFactory(asyncHttpClient, new FetcherSettings());
        this.api = Smart.builder()
                .withParallelFetcherFactory(parallelFetcherFactory)
                .withProfileName("avatars_service")
                .withBaseUrl(writeServiceUri)
                .useTvm(tvmIntegration, tvmService)
                .build()
                .create(Api.class);
    }

    public interface Api {
        @GET("/getimageinfo-{namespace}/{group_id}/{id}")
        @Json
        Call<AvatarsPutCanvasResult> getImageInfo(@Path("namespace") String namespace,
                                                  @Path("group_id") String groupId,
                                                  @Path("id") String id);

        @POST("/put-{namespace}/")
        @Multipart
        @Headers("Content-type: multipart/form-data")
        Call<String> uploadImage(@Path("namespace") String namespace,
                                 @Part PartBase fileData);
    }

    void setUrls(@Nullable AvatarsPutCanvasResult result) {
        if (result == null) {
            return;
        }
        for (AvatarsPutCanvasResult.SizeInfo info : result.getSizes().allSizes()) {
            info.setUrl(readServiceUri + info.getPath());
        }
    }

    @Retryable()
    public AvatarsPutCanvasResult upload(String imageUrl) {

        AvatarsPutCanvasResult result;
        Matcher matchResult = avatarsReadPattern.matcher(imageUrl);

        if (matchResult.matches()) {
            result = getInfoFromNamespace(this.avatarsNamespace, matchResult.group(1), matchResult.group(2));
        } else {
            result = uploadToNamespace(imageUrl, this.avatarsNamespace, AvatarsPutCanvasResult.class);
        }

        setUrls(result);

        return result;
    }

    @Retryable()
    public AvatarsPutCanvasResult upload(String imageName, byte[] data) {
        AvatarsPutCanvasResult result =
                uploadToNamespace(imageName, data, this.avatarsNamespace, AvatarsPutCanvasResult.class);
        setUrls(result);

        return result;
    }

    @Retryable()
    public AvatarsPutCanvasResult upload(String imageName, byte[] data, String namespace) {
        AvatarsPutCanvasResult result =
                uploadToNamespace(imageName, data, namespace, AvatarsPutCanvasResult.class);
        setUrls(result);

        return result;
    }

    @Retryable()
    AvatarsPutTurboResult uploadTurbo(String imageUrl) {
        return uploadToNamespace(imageUrl, TURBO_NAMESPACE, AvatarsPutTurboResult.class);
    }

    @Retryable()
    public byte[] getImageByUrl(String url) {
        logger.info("getting image from avatars: {}", url);

        Result<ParsableBytesRequest.Byteswrapper> result;
        try (TraceProfile profile = Trace.current().profile("avatars_service:getImageByUrl");
             ParallelFetcher<ParsableBytesRequest.Byteswrapper> fetcher =
                     parallelFetcherFactory.getParallelFetcherWithMetricRegistry(
                             SolomonUtils.getParallelFetcherMetricRegistry(profile.getFunc()))) {

            ParsableBytesRequest parsableRequest = new ParsableBytesRequest(0,
                    new RequestBuilder().setUrl(url).build());

            result = fetcher.execute(parsableRequest);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new InterruptedRuntimeException(e);
        } catch (RuntimeException e) {
            throw new AvatarsApiException("Exception during downloading image", e);
        }

        checkResultForErrors(result, error -> new AvatarsApiException("Failed to download image: " + error));
        return result.getSuccess().getContent();
    }

    private AvatarsPutCanvasResult getInfoFromNamespace(String namespace, String groupId, String id) {
        final Stopwatch avatarsUploadStopWatch = Stopwatch.createStarted();

        Call<AvatarsPutCanvasResult> imageInfoCall = api.getImageInfo(namespace, groupId, id);
        logger.info("Getting image info from avatars, url: {}", imageInfoCall.getRequest().getAHCRequest().getUrl());

        Result<AvatarsPutCanvasResult> imageInfoResult = imageInfoCall.execute();
        checkResultForErrors(imageInfoResult, error -> new AvatarsApiException("Failed to get image info: " + error));

        AvatarsPutCanvasResult result = imageInfoResult.getSuccess();

        logger.info("avatars getimageinfo result: {}", result);

        logger.info(PERFORMANCE, "avatars_getimageinfo:{}", avatarsUploadStopWatch.elapsed(MILLISECONDS));

        return result;
    }


    private <T extends AvatarsPutResult> T uploadToNamespace(String imageUrl, String namespace, Class<T> responseClazz) {
        byte[] image = getImageByUrl(imageUrl);
        String imageName = getMd5HashAsHexString(image);
        return uploadToNamespace(imageName, image, namespace, responseClazz);
    }

    private <T extends AvatarsPutResult> T uploadToNamespace(String imageName, byte[] data,
                                                             String namespace, Class<T> responseClazz) {
        final Stopwatch avatarsUploadStopWatch = Stopwatch.createStarted();

        Call<String> uploadImageCall = api.uploadImage(namespace, new ByteArrayPart("file", data,
                "multipart/form-data", StandardCharsets.UTF_8, imageName));

        logger.trace("avatars request uri {}", uploadImageCall.getRequest().getAHCRequest().getUrl());

        Result<String> uploadImageResult = uploadImageCall.execute();
        checkResultForErrors(uploadImageResult, error -> new AvatarsApiException("Failed to upload image: " + error));

        final T result;
        try {
            result = JsonUtils.fromJson(uploadImageResult.getSuccess(), responseClazz);
        } catch (Exception e) {
            throw new AvatarsApiException("Can't parse upload image result", e);
        }

        logger.info("avatars put result: {}", result);

        logger.info(PERFORMANCE, "avatars_upload:{}", avatarsUploadStopWatch.elapsed(MILLISECONDS));

        return result;
    }

    String getReadServiceUri() {
        return readServiceUri;
    }

    public String getReadServiceHost() {
        return readServiceHost;
    }
}
