#include <lighttpd/stream_http_response.h>

typedef struct liStreamHttpResponse liStreamHttpResponse;

struct liStreamHttpResponse {
	liHttpResponseCtx parse_response_ctx;

	liStream stream;
	liVRequest *vr;
	gboolean keepalive, response_headers_finished, transfer_encoding_chunked, wait_for_close;
	goffset content_length;
	liFilterChunkedDecodeState chunked_decode_state;
};

static void check_response_header(liStreamHttpResponse* shr) {
	liResponse *resp = &shr->vr->response;
	GList *l;

	shr->transfer_encoding_chunked = FALSE;
	/* if protocol doesn't support keep-alive just wait for stream end */
	shr->wait_for_close = !shr->keepalive;
	shr->content_length = -1;

	/* Transfer-Encoding: chunked */
	l = li_http_header_find_first(resp->headers, CONST_STR_LEN("transfer-encoding"));
	if (l) {
		for ( ; l ; l = li_http_header_find_next(l, CONST_STR_LEN("transfer-encoding")) ) {
			liHttpHeader *hh = (liHttpHeader*) l->data;
			if (0 == g_ascii_strcasecmp( LI_HEADER_VALUE(hh), "identity" )) {
				/* ignore */
				continue;
			} if (0 == g_ascii_strcasecmp( LI_HEADER_VALUE(hh), "chunked" )) {
				if (shr->transfer_encoding_chunked) {
					VR_ERROR(shr->vr, "%s", "Response is chunked encoded twice");
					li_vrequest_error(shr->vr);
					return;
				}
				shr->transfer_encoding_chunked = TRUE;
			} else {
				VR_ERROR(shr->vr, "Response has unsupported Transfer-Encoding: %s", LI_HEADER_VALUE(hh));
				li_vrequest_error(shr->vr);
				return;
			}
		}
		li_http_header_remove(resp->headers, CONST_STR_LEN("transfer-encoding"));
		/* any non trivial transfer-encoding overwrites content-length */
		if (shr->transfer_encoding_chunked) {
			li_http_header_remove(resp->headers, CONST_STR_LEN("content-length"));
		}
	}

	/* Upgrade: */
	l = li_http_header_find_first(resp->headers, CONST_STR_LEN("upgrade"));
	if (l) {
		gboolean have_connection_upgrade = FALSE;
		liHttpHeaderTokenizer header_tokenizer;
		GString *token;
		if (101 != resp->http_status) {
			VR_ERROR(shr->vr, "Upgrade but status is %i instead of 101 'Switching Protocols'", resp->http_status);
			li_vrequest_error(shr->vr);
			return;
		}
		if (shr->transfer_encoding_chunked) {
			VR_ERROR(shr->vr, "%s", "Upgrade with Transfer-Encoding: chunked");
			li_vrequest_error(shr->vr);
			return;
		}
		/* requires Connection: Upgrade header */
		token = g_string_sized_new(15);
		li_http_header_tokenizer_start(&header_tokenizer, resp->headers, CONST_STR_LEN("Connection"));
		while (li_http_header_tokenizer_next(&header_tokenizer, token)) {
			VR_DEBUG(shr->vr, "Parsing header '%s'", ((liHttpHeader*)header_tokenizer.cur->data)->data->str);
			VR_DEBUG(shr->vr, "Connection token '%s'", token->str);
			if (0 == g_ascii_strcasecmp(token->str, "Upgrade")) {
				have_connection_upgrade = TRUE;
				break;
			}
		}
		g_string_free(token, TRUE); token = NULL;
		if (!have_connection_upgrade) {
			VR_ERROR(shr->vr, "%s", "Upgrade without Connection: Upgrade Transfer");
			li_vrequest_error(shr->vr);
			return;
		}
		shr->wait_for_close = TRUE; /* forward all data until stream closes */
		shr->response_headers_finished = TRUE;
		shr->vr->backend_drain->out->is_closed = FALSE;
		{
			/* li_vrequest_connection_upgrade releases vr->backend_drain; keep our own reference */
			liStream *backend_drain = shr->vr->backend_drain;
			shr->vr->backend_drain = NULL;
			li_vrequest_connection_upgrade(shr->vr, backend_drain, &shr->stream);
			li_stream_release(backend_drain);
		}
		return;
	}

	if (!shr->transfer_encoding_chunked && shr->keepalive) {
		/**
		 * if protocol has HTTP "keepalive" concept and encoding isn't chunked,
		 * we need to check for content-length or "connection: close" indications.
		 * otherwise we won't know when the response is done
		 */
		liHttpHeader *hh;

		switch (shr->parse_response_ctx.http_version) {
		case LI_HTTP_VERSION_1_0:
			if (!li_http_header_is(shr->vr->response.headers, CONST_STR_LEN("connection"), CONST_STR_LEN("keep-alive")))
				shr->wait_for_close = TRUE;
			break;
		case LI_HTTP_VERSION_1_1:
			if (li_http_header_is(shr->vr->response.headers, CONST_STR_LEN("connection"), CONST_STR_LEN("close")))
				shr->wait_for_close = TRUE;
			break;
		case LI_HTTP_VERSION_UNSET:
			break;
		}

		/* content-length */
		hh = li_http_header_lookup(shr->vr->response.headers, CONST_STR_LEN("content-length"));
		if (hh) {
			const gchar *val = LI_HEADER_VALUE(hh);
			gint64 r;
			char *err;

			r = g_ascii_strtoll(val, &err, 10);
			if (*err != '\0') {
				VR_ERROR(shr->vr, "Backend response: content-length is not a number: %s", err);
				li_vrequest_error(shr->vr);
				return;
			}

			/**
			 * negative content-length is not supported
			 * and is a bad request
			 */
			if (r < 0) {
				VR_ERROR(shr->vr, "%s", "Backend response: content-length is negative");
				li_vrequest_error(shr->vr);
				return;
			}

			/**
			 * check if we had a over- or underrun in the string conversion
			 */
			if (r == G_MININT64 || r == G_MAXINT64) {
				if (errno == ERANGE) {
					VR_ERROR(shr->vr, "%s", "Backend response: content-length overflow");
					li_vrequest_error(shr->vr);
					return;
				}
			}

			shr->content_length = r;
			shr->wait_for_close = FALSE;
		}

		if (!shr->wait_for_close && shr->content_length < 0) {
			if (LI_HTTP_VERSION_1_1 == shr->parse_response_ctx.http_version) {
				/* Fallback to waiting for connection close */
				VR_DEBUG(shr->vr, "%s", "HTTP/1.1 response: no clean message length indicator, fall back to waiting for connection close");
				shr->wait_for_close = TRUE;
			} else {
				VR_ERROR(shr->vr, "%s", "Backend: need chunked transfer-encoding or content-length for keepalive connections");
				li_vrequest_error(shr->vr);
				return;
			}
		}
	}

	shr->response_headers_finished = TRUE;
	li_vrequest_indirect_headers_ready(shr->vr);

	return;
}

static void stream_http_response_data(liStreamHttpResponse* shr) {
	if (NULL == shr->stream.source) return;

	if (!shr->response_headers_finished) {
		switch (li_http_response_parse(shr->vr, &shr->parse_response_ctx)) {
		case LI_HANDLER_GO_ON:
			check_response_header(shr);
			if (NULL == shr->stream.source) return;
			break;
		case LI_HANDLER_ERROR:
			VR_ERROR(shr->vr, "%s", "Parsing response header failed");
			li_vrequest_error(shr->vr);
			return;
		case LI_HANDLER_WAIT_FOR_EVENT:
			if (shr->stream.source->out->is_closed) {
				VR_ERROR(shr->vr, "%s", "Parsing response header failed (eos)");
				li_vrequest_error(shr->vr);
			}
			return;
		default:
			return;
		}
	}

	if (shr->transfer_encoding_chunked) {
		if (!li_filter_chunked_decode(shr->vr, shr->stream.out, shr->stream.source->out, &shr->chunked_decode_state)) {
			if (NULL != shr->vr) {
				VR_ERROR(shr->vr, "%s", "Decoding chunks failed");
				li_vrequest_error(shr->vr);
			} else {
				li_stream_reset(&shr->stream);
			}
		} else if (shr->stream.source->out->is_closed) {
			li_stream_disconnect(&shr->stream);
		}
	} else if (shr->wait_for_close) {
		li_chunkqueue_steal_all(shr->stream.out, shr->stream.source->out);
		if (shr->stream.source->out->is_closed) {
			shr->stream.out->is_closed = TRUE;
			li_stream_disconnect(&shr->stream);
		}
	} else {
		g_assert(shr->content_length >= 0);
		if (shr->content_length > 0) {
			goffset moved;
			moved = li_chunkqueue_steal_len(shr->stream.out, shr->stream.source->out, shr->content_length);
			shr->content_length -= moved;
		}
		if (shr->content_length == 0) {
			shr->stream.out->is_closed = TRUE;
			li_stream_disconnect(&shr->stream);
		}
	}
	li_stream_notify(&shr->stream);
}


static void stream_http_response_cb(liStream *stream, liStreamEvent event) {
	liStreamHttpResponse* shr = LI_CONTAINER_OF(stream, liStreamHttpResponse, stream);

	switch (event) {
	case LI_STREAM_NEW_DATA:
		stream_http_response_data(shr);
		break;
	case LI_STREAM_DISCONNECTED_DEST:
		shr->vr = NULL;
		li_stream_disconnect(stream);
		break;
	case LI_STREAM_DISCONNECTED_SOURCE:
		shr->vr = NULL;
		if (!stream->out->is_closed) {
			/* "abort" */
			li_stream_disconnect_dest(stream);
		}
		break;
	case LI_STREAM_DESTROY:
		li_http_response_parser_clear(&shr->parse_response_ctx);
		g_slice_free(liStreamHttpResponse, shr);
		break;
	default:
		break;
	}
}

LI_API liStream* li_stream_http_response_handle(liStream *http_in, liVRequest *vr, gboolean accept_cgi, gboolean accept_nph, gboolean keepalive) {
	liStreamHttpResponse *shr = g_slice_new0(liStreamHttpResponse);
	shr->keepalive = keepalive;
	shr->response_headers_finished = FALSE;
	shr->vr = vr;
	li_stream_init(&shr->stream, &vr->wrk->loop, stream_http_response_cb);
	li_http_response_parser_init(&shr->parse_response_ctx, &vr->response, http_in->out,
		accept_cgi, accept_nph);
	li_stream_connect(http_in, &shr->stream);
	return &shr->stream;
}
