package org.zalando.riptide;

import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.zalando.riptide.RequestArguments.Entity;

import javax.annotation.Nullable;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.IF_MATCH;
import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
import static com.google.common.net.HttpHeaders.IF_UNMODIFIED_SINCE;
import static java.lang.String.join;
import static java.time.ZoneOffset.UTC;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static java.util.stream.Collectors.joining;
import static org.zalando.fauxpas.FauxPas.throwingFunction;

@AllArgsConstructor
final class Requester extends AttributeStage {

    private final RequestExecution network;
    private final RequestArguments arguments;
    private final Plugin plugins;

    @Override
    public <T> AttributeStage attribute(final Attribute<T> attribute, final T value) {
        return withArguments(arguments.withAttribute(attribute, value));
    }

    @Override
    public QueryStage queryParam(final String name, final String value) {
        return withArguments(arguments.withQueryParam(name, value));
    }

    @Override
    public QueryStage queryParams(final Multimap<String, String> params) {
        return queryParams(params.asMap());
    }

    @Override
    public QueryStage queryParams(final Map<String, Collection<String>> params) {
        return withArguments(arguments.withQueryParams(params));
    }

    @Override
    public HeaderStage accept(final MediaType acceptableMediaType, final MediaType... acceptableMediaTypes) {
        return accept(Lists.asList(acceptableMediaType, acceptableMediaTypes));
    }

    @Override
    public HeaderStage accept(final Collection<MediaType> acceptableMediaTypes) {
        return header(ACCEPT, acceptableMediaTypes.stream()
                .map(Objects::toString)
                .collect(joining(", ")));
    }

    @Override
    public HeaderStage contentType(final MediaType contentType) {
        return header(CONTENT_TYPE, contentType.toString());
    }

    @Override
    public HeaderStage ifModifiedSince(final OffsetDateTime since) {
        return header(IF_MODIFIED_SINCE, toHttpDate(since));
    }

    @Override
    public HeaderStage ifUnmodifiedSince(final OffsetDateTime since) {
        return header(IF_UNMODIFIED_SINCE, toHttpDate(since));
    }

    private String toHttpDate(final OffsetDateTime dateTime) {
        return RFC_1123_DATE_TIME.format(dateTime.atZoneSameInstant(UTC));
    }

    @Override
    public HeaderStage ifNoneMatch(final String entityTag, final String... entityTags) {
        return ifNoneMatch(Lists.asList(entityTag, entityTags));
    }

    @Override
    public HeaderStage ifNoneMatch(final Collection<String> entityTags) {
        return header(IF_NONE_MATCH, join(", ", entityTags));
    }

    @Override
    public HeaderStage ifMatch(final String entityTag, final String... entityTags) {
        return ifMatch(Lists.asList(entityTag, entityTags));
    }

    @Override
    public HeaderStage ifMatch(final Collection<String> entityTags) {
        return header(IF_MATCH, join(", ", entityTags));
    }

    @Override
    public HeaderStage header(final String name, final String value) {
        return withArguments(arguments.withHeader(name, value));
    }

    @Override
    public HeaderStage headers(final Multimap<String, String> headers) {
        return headers(headers.asMap());
    }

    @Override
    public HeaderStage headers(final Map<String, ? extends Collection<String>> headers) {
        return withArguments(arguments.withHeaders(headers));
    }

    private Requester withArguments(final RequestArguments arguments) {
        return new Requester(network, arguments, plugins);
    }

    @Override
    public CompletableFuture<ClientHttpResponse> call(final Route route) {
        return body(null).call(route);
    }

    @Override
    public DispatchStage body(@Nullable final Entity entity) {
        return new ResponseDispatcher(arguments.withEntity(entity));
    }

    @Override
    public <T> DispatchStage body(@Nullable final T body) {
        return new ResponseDispatcher(arguments.withBody(body));
    }

    @AllArgsConstructor
    private final class ResponseDispatcher extends DispatchStage {

        private final RequestArguments arguments;

        @Override
        public CompletableFuture<ClientHttpResponse> call(final Route route) {
            final RequestExecution execution =
                    plugins.aroundAsync(
                            plugins.aroundDispatch(
                                    plugins.aroundSerialization(
                                            plugins.aroundNetwork(
                                                    network))));

            return throwingFunction(execution::execute).apply(arguments.withRoute(route));
        }

    }

}