/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */
package com.apigee.sdk.apm.http.impl.client.cache;

import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.ProtocolException;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.protocol.HTTP;

import com.apigee.sdk.apm.http.annotation.Immutable;
import com.apigee.sdk.apm.http.client.cache.HeaderConstants;

/**
 * @since 4.1
 */
@Immutable
class RequestProtocolCompliance {

	/**
	 * Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not and if
	 * not, we can not continue.
	 * 
	 * @param request
	 *            the HttpRequest Object
	 * @return list of {@link RequestProtocolError}
	 */
	public List<RequestProtocolError> requestIsFatallyNonCompliant(
			HttpRequest request) {
		List<RequestProtocolError> theErrors = new ArrayList<RequestProtocolError>();

		// RequestProtocolError anError =
		// requestContainsBodyButNoLength(request);
		// if (anError != null) {
		// theErrors.add(anError);
		// }

		RequestProtocolError anError = requestHasWeakETagAndRange(request);
		if (anError != null) {
			theErrors.add(anError);
		}

		anError = requestHasWeekETagForPUTOrDELETEIfMatch(request);
		if (anError != null) {
			theErrors.add(anError);
		}

		anError = requestContainsNoCacheDirectiveWithFieldName(request);
		if (anError != null) {
			theErrors.add(anError);
		}

		return theErrors;
	}

	/**
	 * If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and
	 * fix the request here. Returning the updated one.
	 * 
	 * @param request
	 *            the request to check for compliance
	 * @return the updated request
	 * @throws ProtocolException
	 *             when we have trouble making the request compliant
	 */
	public HttpRequest makeRequestCompliant(HttpRequest request)
			throws ProtocolException {
		if (requestMustNotHaveEntity(request)) {
			((HttpEntityEnclosingRequest) request).setEntity(null);
		}

		verifyRequestWithExpectContinueFlagHas100continueHeader(request);
		verifyOPTIONSRequestWithBodyHasContentType(request);
		decrementOPTIONSMaxForwardsIfGreaterThen0(request);

		if (requestVersionIsTooLow(request)) {
			return upgradeRequestTo(request, HttpVersion.HTTP_1_1);
		}

		if (requestMinorVersionIsTooHighMajorVersionsMatch(request)) {
			return downgradeRequestTo(request, HttpVersion.HTTP_1_1);
		}

		return request;
	}

	private boolean requestMustNotHaveEntity(HttpRequest request) {
		return HeaderConstants.TRACE_METHOD.equals(request.getRequestLine()
				.getMethod()) && request instanceof HttpEntityEnclosingRequest;
	}

	private void decrementOPTIONSMaxForwardsIfGreaterThen0(HttpRequest request) {
		if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine()
				.getMethod())) {
			return;
		}

		Header maxForwards = request
				.getFirstHeader(HeaderConstants.MAX_FORWARDS);
		if (maxForwards == null) {
			return;
		}

		request.removeHeaders(HeaderConstants.MAX_FORWARDS);
		int currentMaxForwards = Integer.parseInt(maxForwards.getValue());

		request.setHeader(HeaderConstants.MAX_FORWARDS,
				Integer.toString(currentMaxForwards - 1));
	}

	private void verifyOPTIONSRequestWithBodyHasContentType(HttpRequest request) {
		if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine()
				.getMethod())) {
			return;
		}

		if (!(request instanceof HttpEntityEnclosingRequest)) {
			return;
		}

		addContentTypeHeaderIfMissing((HttpEntityEnclosingRequest) request);
	}

	private void addContentTypeHeaderIfMissing(
			HttpEntityEnclosingRequest request) {
		if (request.getEntity().getContentType() == null) {
			((AbstractHttpEntity) request.getEntity())
					.setContentType(HTTP.OCTET_STREAM_TYPE);
		}
	}

	private void verifyRequestWithExpectContinueFlagHas100continueHeader(
			HttpRequest request) {
		if (request instanceof HttpEntityEnclosingRequest) {

			if (((HttpEntityEnclosingRequest) request).expectContinue()
					&& ((HttpEntityEnclosingRequest) request).getEntity() != null) {
				add100ContinueHeaderIfMissing(request);
			} else {
				remove100ContinueHeaderIfExists(request);
			}
		} else {
			remove100ContinueHeaderIfExists(request);
		}
	}

	private void remove100ContinueHeaderIfExists(HttpRequest request) {
		boolean hasHeader = false;

		Header[] expectHeaders = request.getHeaders(HTTP.EXPECT_DIRECTIVE);
		List<HeaderElement> expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();

		for (Header h : expectHeaders) {
			for (HeaderElement elt : h.getElements()) {
				if (!(HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName()))) {
					expectElementsThatAreNot100Continue.add(elt);
				} else {
					hasHeader = true;
				}
			}

			if (hasHeader) {
				request.removeHeader(h);
				for (HeaderElement elt : expectElementsThatAreNot100Continue) {
					BasicHeader newHeader = new BasicHeader(
							HTTP.EXPECT_DIRECTIVE, elt.getName());
					request.addHeader(newHeader);
				}
				return;
			} else {
				expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();
			}
		}
	}

	private void add100ContinueHeaderIfMissing(HttpRequest request) {
		boolean hasHeader = false;

		for (Header h : request.getHeaders(HTTP.EXPECT_DIRECTIVE)) {
			for (HeaderElement elt : h.getElements()) {
				if (HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName())) {
					hasHeader = true;
				}
			}
		}

		if (!hasHeader) {
			request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
		}
	}

	private HttpRequest upgradeRequestTo(HttpRequest request,
			ProtocolVersion version) throws ProtocolException {
		RequestWrapper newRequest = new RequestWrapper(request);
		newRequest.setProtocolVersion(version);

		return newRequest;
	}

	private HttpRequest downgradeRequestTo(HttpRequest request,
			ProtocolVersion version) throws ProtocolException {
		RequestWrapper newRequest = new RequestWrapper(request);
		newRequest.setProtocolVersion(version);

		return newRequest;
	}

	protected boolean requestMinorVersionIsTooHighMajorVersionsMatch(
			HttpRequest request) {
		ProtocolVersion requestProtocol = request.getProtocolVersion();
		if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) {
			return false;
		}

		if (requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor()) {
			return true;
		}

		return false;
	}

	protected boolean requestVersionIsTooLow(HttpRequest request) {
		return request.getProtocolVersion().compareToVersion(
				HttpVersion.HTTP_1_1) < 0;
	}

	/**
	 * Extract error information about the {@link HttpRequest} telling the
	 * 'caller' that a problem occured.
	 * 
	 * @param errorCheck
	 *            What type of error should I get
	 * @return The {@link HttpResponse} that is the error generated
	 */
	public HttpResponse getErrorForRequest(RequestProtocolError errorCheck) {
		switch (errorCheck) {
		case BODY_BUT_NO_LENGTH_ERROR:
			return new BasicHttpResponse(new BasicStatusLine(
					HttpVersion.HTTP_1_1, HttpStatus.SC_LENGTH_REQUIRED, ""));

		case WEAK_ETAG_AND_RANGE_ERROR:
			return new BasicHttpResponse(new BasicStatusLine(
					HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST,
					"Weak eTag not compatible with byte range"));

		case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR:
			return new BasicHttpResponse(new BasicStatusLine(
					HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST,
					"Weak eTag not compatible with PUT or DELETE requests"));

		case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME:
			return new BasicHttpResponse(new BasicStatusLine(
					HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST,
					"No-Cache directive MUST NOT include a field name"));

		default:
			throw new IllegalStateException(
					"The request was compliant, therefore no error can be generated for it.");

		}
	}

	private RequestProtocolError requestHasWeakETagAndRange(HttpRequest request) {
		// TODO: Should these be looking at all the headers marked as Range?
		String method = request.getRequestLine().getMethod();
		if (!(HeaderConstants.GET_METHOD.equals(method))) {
			return null;
		}

		Header range = request.getFirstHeader(HeaderConstants.RANGE);
		if (range == null)
			return null;

		Header ifRange = request.getFirstHeader(HeaderConstants.IF_RANGE);
		if (ifRange == null)
			return null;

		String val = ifRange.getValue();
		if (val.startsWith("W/")) {
			return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR;
		}

		return null;
	}

	private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch(
			HttpRequest request) {
		// TODO: Should these be looking at all the headers marked as
		// If-Match/If-None-Match?

		String method = request.getRequestLine().getMethod();
		if (!(HeaderConstants.PUT_METHOD.equals(method) || HeaderConstants.DELETE_METHOD
				.equals(method))) {
			return null;
		}

		Header ifMatch = request.getFirstHeader(HeaderConstants.IF_MATCH);
		if (ifMatch != null) {
			String val = ifMatch.getValue();
			if (val.startsWith("W/")) {
				return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
			}
		} else {
			Header ifNoneMatch = request
					.getFirstHeader(HeaderConstants.IF_NONE_MATCH);
			if (ifNoneMatch == null)
				return null;

			String val2 = ifNoneMatch.getValue();
			if (val2.startsWith("W/")) {
				return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
			}
		}

		return null;
	}

	private RequestProtocolError requestContainsNoCacheDirectiveWithFieldName(
			HttpRequest request) {
		for (Header h : request.getHeaders("Cache-Control")) {
			for (HeaderElement elt : h.getElements()) {
				if ("no-cache".equalsIgnoreCase(elt.getName())
						&& elt.getValue() != null) {
					return RequestProtocolError.NO_CACHE_DIRECTIVE_WITH_FIELD_NAME;
				}
			}
		}
		return null;
	}
}