/*
 * Asterisk -- An open source telephony toolkit.
 *
 * Copyright (C) 2013, Digium, Inc.
 *
 * David M. Lee, II <dlee@digium.com>
 *
 * See http://www.asterisk.org for more information about
 * the Asterisk project. Please do not directly contact
 * any of the maintainers of this project for assistance;
 * the project provides a web site, mailing lists and IRC
 * channels for your use.
 *
 * This program is free software, distributed under the terms of
 * the GNU General Public License Version 2. See the LICENSE file
 * at the top of the source tree.
 */

#include "asterisk.h"

#include "asterisk/ari.h"
#include "asterisk/astobj2.h"
#include "asterisk/http_websocket.h"
#include "asterisk/stasis_app.h"
#include "internal.h"

/*! \file
 *
 * \brief WebSocket support for RESTful API's.
 * \author David M. Lee, II <dlee@digium.com>
 */

struct ast_ari_websocket_session {
	struct ast_websocket *ws_session;
	int (*validator)(struct ast_json *);
};

static void websocket_session_dtor(void *obj)
{
	struct ast_ari_websocket_session *session = obj;

	ast_websocket_unref(session->ws_session);
	session->ws_session = NULL;
}

/*!
 * \brief Validator that always succeeds.
 */
static int null_validator(struct ast_json *json)
{
	return 1;
}

struct ast_ari_websocket_session *ast_ari_websocket_session_create(
	struct ast_websocket *ws_session, int (*validator)(struct ast_json *))
{
	RAII_VAR(struct ast_ari_websocket_session *, session, NULL, ao2_cleanup);
	RAII_VAR(struct ast_ari_conf *, config, ast_ari_config_get(), ao2_cleanup);

	if (ws_session == NULL) {
		return NULL;
	}

	if (config == NULL || config->general == NULL) {
		return NULL;
	}

	if (validator == NULL) {
		validator = null_validator;
	}

	if (ast_websocket_set_nonblock(ws_session) != 0) {
		ast_log(LOG_ERROR,
			"ARI web socket failed to set nonblock; closing: %s\n",
			strerror(errno));
		return NULL;
	}

	if (ast_websocket_set_timeout(ws_session, config->general->write_timeout)) {
		ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n",
			config->general->write_timeout);
	}

	session = ao2_alloc(sizeof(*session), websocket_session_dtor);
	if (!session) {
		return NULL;
	}

	ao2_ref(ws_session, +1);
	session->ws_session = ws_session;
	session->validator = validator;

	ao2_ref(session, +1);
	return session;
}

struct ast_json *ast_ari_websocket_session_read(
	struct ast_ari_websocket_session *session)
{
	RAII_VAR(struct ast_json *, message, NULL, ast_json_unref);

	if (ast_websocket_fd(session->ws_session) < 0) {
		return NULL;
	}

	while (!message) {
		int res;
		char *payload;
		uint64_t payload_len;
		enum ast_websocket_opcode opcode;
		int fragmented;

		res = ast_wait_for_input(
			ast_websocket_fd(session->ws_session), -1);

		if (res <= 0) {
			ast_log(LOG_WARNING, "WebSocket poll error: %s\n",
				strerror(errno));
			return NULL;
		}

		res = ast_websocket_read(session->ws_session, &payload,
			&payload_len, &opcode, &fragmented);

		if (res != 0) {
			ast_log(LOG_WARNING, "WebSocket read error: %s\n",
				strerror(errno));
			return NULL;
		}

		switch (opcode) {
		case AST_WEBSOCKET_OPCODE_CLOSE:
			ast_debug(1, "WebSocket closed\n");
			return NULL;
		case AST_WEBSOCKET_OPCODE_TEXT:
			message = ast_json_load_buf(payload, payload_len, NULL);
			if (message == NULL) {
				ast_log(LOG_WARNING,
					"WebSocket input failed to parse\n");
			}

			break;
		default:
			/* Ignore all other message types */
			break;
		}
	}

	return ast_json_ref(message);
}

#define VALIDATION_FAILED				\
	"{"						\
	"  \"error\": \"InvalidMessage\","		\
	"  \"message\": \"Message validation failed\""	\
	"}"

int ast_ari_websocket_session_write(struct ast_ari_websocket_session *session,
	struct ast_json *message)
{
	RAII_VAR(char *, str, NULL, ast_json_free);

#ifdef AST_DEVMODE
	if (!session->validator(message)) {
		ast_log(LOG_ERROR, "Outgoing message failed validation\n");
		return ast_websocket_write_string(session->ws_session, VALIDATION_FAILED);
	}
#endif

	str = ast_json_dump_string_format(message, ast_ari_json_format());

	if (str == NULL) {
		ast_log(LOG_ERROR, "Failed to encode JSON object\n");
		return -1;
	}

	if (ast_websocket_write_string(session->ws_session, str)) {
		ast_log(LOG_NOTICE, "Problem occurred during websocket write to %s, websocket closed\n",
			ast_sockaddr_stringify(ast_ari_websocket_session_get_remote_addr(session)));
		return -1;
	}
	return 0;
}

struct ast_sockaddr *ast_ari_websocket_session_get_remote_addr(
	struct ast_ari_websocket_session *session)
{
	return ast_websocket_remote_address(session->ws_session);
}

void ari_handle_websocket(struct ast_websocket_server *ws_server,
	struct ast_tcptls_session_instance *ser, const char *uri,
	enum ast_http_method method, struct ast_variable *get_params,
	struct ast_variable *headers)
{
	struct ast_http_uri fake_urih = {
		.data = ws_server,
	};
	ast_websocket_uri_cb(ser, &fake_urih, uri, method, get_params,
		headers);
}

const char *ast_ari_websocket_session_id(
	const struct ast_ari_websocket_session *session)
{
	return ast_websocket_session_id(session->ws_session);
}
