/**
 * @file soap.c
 * 	C file for SOAP connection related process
 *
 * purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "internal.h"

#include "soap.h"

#include "session.h"

#include "debug.h"
#include "xmlnode.h"

#include <glib.h>
#if !defined(_WIN32) || !defined(_WINERROR_)
#include <error.h>
#endif

#define SOAP_TIMEOUT (5 * 60)

typedef struct _MsnSoapRequest {
	char *path;
	MsnSoapMessage *message;
	MsnSoapCallback cb;
	gpointer cb_data;
} MsnSoapRequest;

typedef struct _MsnSoapConnection {
	MsnSession *session;
	char *host;

	time_t last_used;
	PurpleSslConnection *ssl;
	gboolean connected;

	guint event_handle;
	GString *buf;
	gsize handled_len;
	gsize body_len;
	int response_code;
	gboolean headers_done;
	gboolean close_when_done;

	MsnSoapMessage *message;

	GQueue *queue;
	MsnSoapRequest *current_request;
} MsnSoapConnection;

static void msn_soap_connection_destroy_foreach_cb(gpointer item, gpointer data);
static gboolean msn_soap_connection_run(gpointer data);

static MsnSoapConnection *msn_soap_connection_new(MsnSession *session,
	const char *host);
static void msn_soap_connection_handle_next(MsnSoapConnection *conn);
static void msn_soap_connection_destroy(MsnSoapConnection *conn);

static void msn_soap_message_send_internal(MsnSession *session,
	MsnSoapMessage *message, const char *host, const char *path,
	MsnSoapCallback cb, gpointer cb_data, gboolean first);

static void msn_soap_request_destroy(MsnSoapRequest *req, gboolean keep_message);
static void msn_soap_connection_sanitize(MsnSoapConnection *conn, gboolean disconnect);
static gboolean msn_soap_write_cb_internal(gpointer data, gint fd, PurpleInputCondition cond, gboolean initial);
static void msn_soap_process(MsnSoapConnection *conn);

static gboolean
msn_soap_cleanup_each(gpointer key, gpointer value, gpointer data)
{
	MsnSoapConnection *conn = value;
	time_t *t = data;

	if ((*t - conn->last_used) > SOAP_TIMEOUT * 2) {
		purple_debug_info("soap", "cleaning up soap conn %p\n", conn);
		return TRUE;
	}

	return FALSE;
}

static gboolean
msn_soap_cleanup_for_session(gpointer data)
{
	MsnSession *sess = data;
	time_t t = time(NULL);

	purple_debug_info("soap", "session cleanup timeout\n");

	if (sess->soap_table) {
		g_hash_table_foreach_remove(sess->soap_table, msn_soap_cleanup_each,
			&t);

		if (g_hash_table_size(sess->soap_table) == 0) {
			purple_timeout_remove(sess->soap_cleanup_handle);
			sess->soap_cleanup_handle = 0;
		}
	}

	return TRUE;
}

static MsnSoapConnection *
msn_soap_get_connection(MsnSession *session, const char *host)
{
	MsnSoapConnection *conn = NULL;

	if (session->soap_table) {
		conn = g_hash_table_lookup(session->soap_table, host);
	} else {
		session->soap_table = g_hash_table_new_full(g_str_hash, g_str_equal,
			NULL, (GDestroyNotify)msn_soap_connection_destroy);
	}

	if (session->soap_cleanup_handle == 0)
		session->soap_cleanup_handle = purple_timeout_add(SOAP_TIMEOUT * 1000,
			msn_soap_cleanup_for_session, session);

	if (conn == NULL) {
		conn = msn_soap_connection_new(session, host);
		g_hash_table_insert(session->soap_table, conn->host, conn);
	}

	conn->last_used = time(NULL);

	return conn;
}

static MsnSoapConnection *
msn_soap_connection_new(MsnSession *session, const char *host)
{
	MsnSoapConnection *conn = g_new0(MsnSoapConnection, 1);
	conn->session = session;
	conn->host = g_strdup(host);
	conn->queue = g_queue_new();
	return conn;
}

static void
msn_soap_connected_cb(gpointer data, PurpleSslConnection *ssl,
		PurpleInputCondition cond)
{
	MsnSoapConnection *conn = data;

	conn->connected = TRUE;

	if (conn->event_handle == 0)
		conn->event_handle = purple_timeout_add(0, msn_soap_connection_run, conn);
}

static void
msn_soap_error_cb(PurpleSslConnection *ssl, PurpleSslErrorType error,
		gpointer data)
{
	MsnSoapConnection *conn = data;

	/* sslconn already frees the connection in case of error */
	conn->ssl = NULL;

	g_hash_table_remove(conn->session->soap_table, conn->host);
}

static gboolean
msn_soap_handle_redirect(MsnSoapConnection *conn, const char *url)
{
	char *host;
	char *path;

	if (purple_url_parse(url, &host, NULL, &path, NULL, NULL)) {
		msn_soap_message_send_internal(conn->session,
			conn->current_request->message,	host, path,
			conn->current_request->cb, conn->current_request->cb_data, TRUE);

		msn_soap_request_destroy(conn->current_request, TRUE);
		conn->current_request = NULL;

		g_free(host);
		g_free(path);

		return TRUE;
	}

	return FALSE;
}

static gboolean
msn_soap_handle_body(MsnSoapConnection *conn, MsnSoapMessage *response)
{
	xmlnode *body = xmlnode_get_child(response->xml, "Body");
	xmlnode *fault = xmlnode_get_child(response->xml, "Fault");

	if (fault) {
		xmlnode *faultcode = xmlnode_get_child(fault, "faultcode");

		if (faultcode != NULL) {
			char *faultdata = xmlnode_get_data(faultcode);

			if (g_str_equal(faultdata, "psf:Redirect")) {
				xmlnode *url = xmlnode_get_child(fault, "redirectUrl");

				if (url) {
					char *urldata = xmlnode_get_data(url);
					msn_soap_handle_redirect(conn, urldata);
					g_free(urldata);
				}

				g_free(faultdata);
				msn_soap_message_destroy(response);
				return TRUE;
			} else if (g_str_equal(faultdata, "wsse:FailedAuthentication")) {
				xmlnode *reason = xmlnode_get_child(fault, "faultstring");
				char *reasondata = xmlnode_get_data(reason);

				msn_soap_connection_sanitize(conn, TRUE);
				msn_session_set_error(conn->session, MSN_ERROR_AUTH,
					reasondata);

				g_free(reasondata);
				g_free(faultdata);
				msn_soap_message_destroy(response);
				return FALSE;
			}

			g_free(faultdata);
		}
	}

	if (fault || body) {
		MsnSoapRequest *request = conn->current_request;
		conn->current_request = NULL;
		request->cb(request->message, response,
			request->cb_data);
		msn_soap_message_destroy(response);
		msn_soap_request_destroy(request, FALSE);
	}

	return TRUE;
}

static void
msn_soap_read_cb(gpointer data, gint fd, PurpleInputCondition cond)
{
	MsnSoapConnection *conn = data;
	int count = 0, cnt, perrno;
	/* This buffer needs to be larger than any packets received from
		login.live.com or Adium will fail to receive the packet
		(something weird with the login.live.com server). With NSS it works
		fine, so I believe it's some bug with OS X */ 
	char buf[16 * 1024];

	if (conn->message == NULL) {
		conn->message = msn_soap_message_new(NULL, NULL);
	}

	if (conn->buf == NULL) {
		conn->buf = g_string_new_len(buf, 0);
	}
	
	while ((cnt = purple_ssl_read(conn->ssl, buf, sizeof(buf))) > 0) {
		purple_debug_info("soap", "read %d bytes\n", cnt);
		count += cnt;
		g_string_append_len(conn->buf, buf, cnt);
	}

	/* && count is necessary for Adium, on OS X the last read always
	   return an error, so we want to proceed anyway. See #5212 for
	   discussion on this and the above buffer size issues */
	if(cnt < 0 && errno == EAGAIN && count == 0)
		return;

	/* msn_soap_process could alter errno */
	perrno = errno;
	msn_soap_process(conn);
	
	if (cnt < 0 && perrno != EAGAIN) {
		purple_debug_info("soap", "read: %s\n", g_strerror(perrno));
		/* It's possible msn_soap_process closed the ssl connection */
		if (conn->ssl) {
			purple_ssl_close(conn->ssl);
			conn->ssl = NULL;
			msn_soap_connection_handle_next(conn);
		}
	}
}

static void
msn_soap_process(MsnSoapConnection *conn) {
	gboolean handled = FALSE;
	char *cursor;
	char *linebreak;

	purple_debug_info("soap", "current %s\n", conn->buf->str);

	cursor = conn->buf->str + conn->handled_len;

	if (!conn->headers_done) {
		while ((linebreak = strstr(cursor, "\r\n"))	!= NULL) {
			conn->handled_len = linebreak - conn->buf->str + 2;

			if (conn->response_code == 0) {
				if (sscanf(cursor, "HTTP/1.1 %d", &conn->response_code) != 1) {
					/* something horribly wrong */
					purple_ssl_close(conn->ssl);
					conn->ssl = NULL;
					msn_soap_connection_handle_next(conn);
					handled = TRUE;
					break;
				} else if (conn->response_code == 503) {
					msn_soap_connection_sanitize(conn, TRUE);
					msn_session_set_error(conn->session, MSN_ERROR_SERV_UNAVAILABLE, NULL);
					return;
				}
			} else if (cursor == linebreak) {
				/* blank line */
				conn->headers_done = TRUE;
				cursor = conn->buf->str + conn->handled_len;
				break;
			} else {
				char *line = g_strndup(cursor, linebreak - cursor);
				char *sep = strstr(line, ": ");
				char *key = line;
				char *value;

				if (sep == NULL) {
					purple_debug_info("soap", "ignoring malformed line: %s\n", line);
					g_free(line);
					goto loop_end;
				}

				value = sep + 2;
				*sep = '\0';
				msn_soap_message_add_header(conn->message, key, value);

				if ((conn->response_code == 301 || conn->response_code == 300)
					&& strcmp(key, "Location") == 0) {

					msn_soap_handle_redirect(conn, value);

					handled = TRUE;
					g_free(line);
					break;
				} else if (conn->response_code == 401 &&
					strcmp(key, "WWW-Authenticate") == 0) {
					char *error = strstr(value, "cbtxt=");

					if (error) {
						error += strlen("cbtxt=");
					}

					msn_soap_connection_sanitize(conn, TRUE);
					msn_session_set_error(conn->session, MSN_ERROR_AUTH,
						error ? purple_url_decode(error) : NULL);

					g_free(line);
					return;
				} else if (strcmp(key, "Content-Length") == 0) {
					conn->body_len = atoi(value);
				} else if (strcmp(key, "Connection") == 0) {
					if (strcmp(value, "close") == 0) {
						conn->close_when_done = TRUE;
					}
				}
				g_free(line);
			}

		loop_end:
			cursor = conn->buf->str + conn->handled_len;
		}
	}

	if (!handled && conn->headers_done) {
		if (conn->buf->len - conn->handled_len >=
			conn->body_len) {
			xmlnode *node = xmlnode_from_str(cursor, conn->body_len);

			if (node == NULL) {
				purple_debug_info("soap", "Malformed SOAP response: %s\n",
					cursor);
			} else {
				MsnSoapMessage *message = conn->message;
				conn->message = NULL;
				message->xml = node;

				if (!msn_soap_handle_body(conn, message)) {
					return;
				}
			}

			msn_soap_connection_handle_next(conn);
		}

		return;
	}

	if (handled) {
		msn_soap_connection_handle_next(conn);
	}
}

static void
msn_soap_write_cb(gpointer data, gint fd, PurpleInputCondition cond)
{
	msn_soap_write_cb_internal(data, fd, cond, FALSE);
}

static gboolean
msn_soap_write_cb_internal(gpointer data, gint fd, PurpleInputCondition cond,
		gboolean initial)
{
	MsnSoapConnection *conn = data;
	int written;

	if (cond != PURPLE_INPUT_WRITE) return TRUE;

	written = purple_ssl_write(conn->ssl, conn->buf->str + conn->handled_len,
		conn->buf->len - conn->handled_len);

	if (written < 0 && errno == EAGAIN)
		return TRUE;
	else if (written <= 0) {
		purple_ssl_close(conn->ssl);
		conn->ssl = NULL;
		if (!initial) msn_soap_connection_handle_next(conn);
		return FALSE;
	}

	conn->handled_len += written;

	if (conn->handled_len < conn->buf->len)
		return TRUE;

	/* we are done! */
	g_string_free(conn->buf, TRUE);
	conn->buf = NULL;
	conn->handled_len = 0;
	conn->body_len = 0;
	conn->response_code = 0;
	conn->headers_done = FALSE;
	conn->close_when_done = FALSE;

	purple_input_remove(conn->event_handle);
	conn->event_handle = purple_input_add(conn->ssl->fd, PURPLE_INPUT_READ,
		msn_soap_read_cb, conn);
	return TRUE;
}

static gboolean
msn_soap_connection_run(gpointer data)
{
	MsnSoapConnection *conn = data;
	MsnSoapRequest *req = g_queue_peek_head(conn->queue);

	conn->event_handle = 0;

	if (req) {
		if (conn->ssl == NULL) {
			conn->ssl = purple_ssl_connect(conn->session->account, conn->host,
				443, msn_soap_connected_cb, msn_soap_error_cb, conn);
		} else if (conn->connected) {
			int len = -1;
			char *body = xmlnode_to_str(req->message->xml, &len);
			GSList *iter;

			g_queue_pop_head(conn->queue);

			conn->buf = g_string_new("");

			g_string_append_printf(conn->buf,
				"POST /%s HTTP/1.1\r\n"
				"SOAPAction: %s\r\n"
				"Content-Type:text/xml; charset=utf-8\r\n"
				"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)\r\n"
				"Accept: */*\r\n"
				"Host: %s\r\n"
				"Content-Length: %d\r\n"
				"Connection: Keep-Alive\r\n"
				"Cache-Control: no-cache\r\n",
				req->path, req->message->action ? req->message->action : "",
				conn->host, len);

			for (iter = req->message->headers; iter; iter = iter->next) {
				g_string_append(conn->buf, (char *)iter->data);
				g_string_append(conn->buf, "\r\n");
			}

			g_string_append(conn->buf, "\r\n");
			g_string_append(conn->buf, body);

			purple_debug_info("soap", "%s\n", conn->buf->str);

			conn->handled_len = 0;
			conn->current_request = req;

			conn->event_handle = purple_input_add(conn->ssl->fd,
				PURPLE_INPUT_WRITE, msn_soap_write_cb, conn);
			if (!msn_soap_write_cb_internal(conn, conn->ssl->fd, PURPLE_INPUT_WRITE, TRUE)) {
				/* Not connected => reconnect and retry */
				purple_debug_info("soap", "not connected, reconnecting\n");
				
				conn->connected = FALSE;
				conn->current_request = NULL;
				msn_soap_connection_sanitize(conn, FALSE);
				
				g_queue_push_head(conn->queue, req);
				conn->event_handle = purple_timeout_add(0, msn_soap_connection_run, conn);
			}

			g_free(body);
		}
	}

	return FALSE;
}

void
msn_soap_message_send(MsnSession *session, MsnSoapMessage *message,
	const char *host, const char *path,
	MsnSoapCallback cb, gpointer cb_data)
{
	msn_soap_message_send_internal(session, message, host, path, cb, cb_data,
		FALSE);
}

static void
msn_soap_message_send_internal(MsnSession *session,
	MsnSoapMessage *message, const char *host, const char *path,
	MsnSoapCallback cb, gpointer cb_data, gboolean first)
{
	MsnSoapConnection *conn = msn_soap_get_connection(session, host);
	MsnSoapRequest *req = g_new0(MsnSoapRequest, 1);

	req->path = g_strdup(path);
	req->message = message;
	req->cb = cb;
	req->cb_data = cb_data;

	if (first) {
		g_queue_push_head(conn->queue, req);
	} else {
		g_queue_push_tail(conn->queue, req);
	}

	if (conn->event_handle == 0)
		conn->event_handle = purple_timeout_add(0, msn_soap_connection_run,
			conn);
}

static void
msn_soap_connection_sanitize(MsnSoapConnection *conn, gboolean disconnect)
{
	if (conn->event_handle) {
		purple_input_remove(conn->event_handle);
		conn->event_handle = 0;
	}

	if (conn->message) {
		msn_soap_message_destroy(conn->message);
		conn->message = NULL;
	}

	if (conn->buf) {
		g_string_free(conn->buf, TRUE);
		conn->buf = NULL;
	}

	if (conn->ssl && (disconnect || conn->close_when_done)) {
		purple_ssl_close(conn->ssl);
		conn->ssl = NULL;
	}

	if (conn->current_request) {
		msn_soap_request_destroy(conn->current_request, FALSE);
		conn->current_request = NULL;
	}
}

static void
msn_soap_connection_handle_next(MsnSoapConnection *conn)
{
	msn_soap_connection_sanitize(conn, FALSE);

	conn->event_handle = purple_timeout_add(0, msn_soap_connection_run,	conn);

	if (conn->current_request) {
		MsnSoapRequest *req = conn->current_request;
		conn->current_request = NULL;
		msn_soap_connection_destroy_foreach_cb(req, conn);
	}
}

static void
msn_soap_connection_destroy_foreach_cb(gpointer item, gpointer data)
{
	MsnSoapRequest *req = item;

	if (req->cb)
		req->cb(req->message, NULL, req->cb_data);

	msn_soap_request_destroy(req, FALSE);
}

static void
msn_soap_connection_destroy(MsnSoapConnection *conn)
{
	if (conn->current_request) {
		MsnSoapRequest *req = conn->current_request;
		conn->current_request = NULL;
		msn_soap_connection_destroy_foreach_cb(req, conn);
	}

	msn_soap_connection_sanitize(conn, TRUE);
	g_queue_foreach(conn->queue, msn_soap_connection_destroy_foreach_cb, conn);
	g_queue_free(conn->queue);

	g_free(conn->host);
	g_free(conn);
}

MsnSoapMessage *
msn_soap_message_new(const char *action, xmlnode *xml)
{
	MsnSoapMessage *message = g_new0(MsnSoapMessage, 1);

	message->action = g_strdup(action);
	message->xml = xml;

	return message;
}

void
msn_soap_message_destroy(MsnSoapMessage *message)
{
	if (message) {
		g_slist_foreach(message->headers, (GFunc)g_free, NULL);
		g_slist_free(message->headers);
		g_free(message->action);
		if (message->xml)
			xmlnode_free(message->xml);
		g_free(message);
	}
}

void
msn_soap_message_add_header(MsnSoapMessage *message,
		const char *name, const char *value)
{
	char *header = g_strdup_printf("%s: %s\r\n", name, value);

	message->headers = g_slist_prepend(message->headers, header);
}

static void
msn_soap_request_destroy(MsnSoapRequest *req, gboolean keep_message)
{
	g_free(req->path);
	if (!keep_message)
		msn_soap_message_destroy(req->message);
	g_free(req);
}

