package com.atlassian.stash.internal.rest.inject;

import com.atlassian.stash.internal.rest.annotations.OptionalBody;
import com.sun.jersey.api.container.MappableContainerException;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.core.provider.EntityHolder;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.MessageBodyReader;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * Applies {@link OptionalBody} handling, allowing {@code EntityHolder} parameters on REST resources to
 * work as expected.
 * <p>
 * If this looks vile to you, that's because it <i>is</i>. Let me tell you a tale. A tale of woe, and of hopes
 * shattered. Let me tell you a tale of Atlassian REST.
 * <p>
 * Jersey offers a useful tool, {@code EntityHolder}, for cases where a REST request <i>optionally</i> wants to
 * consume a body. No, let me back up. This is still too late in the story.
 * <p>
 * When pull requests were first shipped, merging a pull request didn't take a body. The version was provided as
 * a query parameter. Most REST tests didn't send any body, and neither did the UI, and the body was ignored for
 * the few tests that did send one.
 * <p>
 * Fast forward to 3.7, and adding support for providing a custom message for the pull request merge commit. Now
 * merging a pull request needs to accept a body. However, for backwards compatibility, it can't <i>require</i>
 * one. Enter {@code EntityHolder}. In theory it should all be so simple, requiring nothing more than adding
 * {@code EntityHolder&lt;RestPullRequestMergeRequest&gt; bodyHolder}, or the the like, to the signature.
 * <p>
 * And that's where it all goes pear-shaped, and {@link OptionalBody} is the final result. Jersey prefers to match
 * the {@code MessageBodyReader} to use by media type, and searches custom readers before built-in readers. The
 * combination of those two preferences means a JSON-parsing reader (Jackson at the time this was written) is
 * always picked. That reader doesn't:
 * <ul>
 *     <li>Understand the idea of a conditional body; it should only be used when a body is present</li>
 *     <li>Parse out the response as a <i>nested entity</i>, since that's not what the JSON looks like, so it
 *     tries to set fields {@code EntityHolder} doesn't have</li>
 * </ul>
 * So add a custom {@code MessageBodyReader}, right? Slow down, cowboy. It's not that simple. The custom reader,
 * like {@code EntityHolderReader}, needs access to the {@code MessageBodyWorkers} so it can get the <i>next</i>
 * reader, the one that actually reads the body when one is provided. Atlassian REST configures Jersey to use
 * Spring for wiring when it instantiates components, which is <i>generally</i> what we want. Except for when
 * you need to use something like {@code MessageBodyWorkers}, which isn't registered in any Spring context, either
 * for <i>this plugin</i> or the Atlassian REST plugin itself.
 * <p>
 * From there you start casting about desperately, wondering where you could get the {@code MessageBodyWorkers}.
 * You see that {@code WebApplication} offers an accessor, but that's <i>also</i> not available in Spring. So
 * you create a marker annotation and a special {@link InjectableProvider} to make it easier to use. And you weep
 * bitter tears as you write some lamentory Javadocs, anticipating just how unimpressed your reviewers will be
 * with the "workaround".
 *
 * @since 3.7
 */
public class OptionalBodyInjectableProvider implements InjectableProvider<OptionalBody, Type> {

    @Override
    public Injectable getInjectable(ComponentContext ic, OptionalBody optionalBody, Type type) {
        if (type instanceof ParameterizedType) {
            ParameterizedType genericType = (ParameterizedType) type;
            if (EntityHolder.class.equals(genericType.getRawType())) {
                return new OptionalBodyInjectable(genericType);
            }
        }

        return null;
    }

    @Override
    public ComponentScope getScope() {
        return ComponentScope.Singleton;
    }

    private static class OptionalBodyInjectable extends AbstractResourceInjectable<EntityHolder> {

        private final Type genericType;

        private OptionalBodyInjectable(Type genericType) {
            this.genericType = genericType;
        }

        @Override
        protected EntityHolder doGetValue(HttpContext context) {
            ContainerRequest request = (ContainerRequest) context.getRequest();

            //This matches the implementation of ContainerRequest.getEntity(Class, Type, Annotation[]), which
            //we can't use directly here because it will never select the EntityHolderReader itself
            MediaType mediaType = request.getMediaType();
            if (mediaType == null) {
                mediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
            }

            //Wildcard MediaType here, rather than the requested MediaType, to get the EntityHolderReader
            MessageBodyReader<EntityHolder> reader = request.getMessageBodyWorkers().getMessageBodyReader(
                    EntityHolder.class, genericType, EMPTY_ANNOTATIONS, MediaType.WILDCARD_TYPE);
            try {
                //The requested MediaType here so that EntityHolderReader's call to getMessageBodyReader
                //will return the right reader to read the body, if there is one
                return reader.readFrom(EntityHolder.class, genericType, EMPTY_ANNOTATIONS, mediaType,
                        request.getRequestHeaders(), request.getEntityInputStream());
            } catch (IOException e) {
                throw new MappableContainerException(e);
            }
        }
    }
}
