/* ARISA - Admin Connectivity
 * Copyright (C) 2003, 2004 Carl Ritson
 *
 * 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
 */

#define WANT_POLL

#include "arisa.h"
#include "user.h"

#include <arpa/inet.h>
#include <sys/stat.h>

#ifdef HAVE_SYS_UN_H

#include <sys/un.h>

#ifndef UNIX_PATH_MAX
/* On Linux its 108 on FreeBSD 104, Help?! */
#define UNIX_PATH_MAX 100
#endif /* !UNIX_PATH_MAX */

#endif /* HAVE_SYS_UN_H */

#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <errno.h>
#include <stdarg.h>
#include <ctype.h>

#include "admin.h"

/** achat_t
 * 	Internal structure for user by admin threads.
 */
typedef struct achat_t {
	int		sok;
	chat_t		*chat;
	user_t		*user;
	pqueue_t	*msgqueue;
	acontext_t	*context;
	char		*sokbuf;
	size_t		sokpos;
} achat_t;

/**********************
 * Exported Functions *
 **********************/

static void *admin_thread(void *arg); // pre-declaration
/** admin_begin
 * 	Initialises an achat_t structure, adds the chat to the global list 
 *	of admin sessions and starts an admin thread associated with it.
 */
void admin_begin(int sok, chat_t *chat) {
	achat_t *ac 	= xalloc(sizeof(achat_t));
	ac->sok 	= sok;
	ac->context	= admin_new_context(&chat->msgqueue);;
	ac->msgqueue	= &chat->msgqueue;
	ac->chat 	= chat;
	ac->sokbuf	= xalloc(1024);
	ac->sokpos	= 0;
	LOCK(global);
	LOCK(chat);
	PTRARR_ADD(&global->admins,&global->no_admins,chat);
	UNLOCK(global);
	ac->user = chat->admin.user;
	ac->context->colour = chat->colour;
	thread_create(&(chat->admin.thread),admin_thread,ac);
	UNLOCK(chat);
}

/** admin_open
 *	Initialises an chat_t structure.
 *	If a socket is not already establisted it attempts to add
 *	the session to a CHAT type interface.
 *	If a socket already exists or no CHAT interface is availible
 *	then admin_begin is called to start the associated thread.
 *	User Logon count is checked here, if exceeded then a thread
 *	won't be started, this prevents a denial of service.
 */
void admin_open(int sok, network_t *net, const char *host, 
			const char *nick, user_t *user) {
	char buffer[512];
	chat_t *chat;
	int i,tmp;

	if(sok == -1 && (net == NULL || nick == NULL))
		return;
	
	if(user != NULL) {
		LOCK(user);
		tmp = user->no_permitted_logons;
		xstrncpy(buffer,user->username,sizeof(buffer));
		UNLOCK(user);
		if(tmp != 0) {
			LOCK(global);
			for(i = 0; i < global->no_admins && tmp > 0; ++i) {
				LOCK(global->admins[i]);
				if(global->admins[i]->state != STATE_COMPLETE &&
					global->admins[i]->state != STATE_DELETED &&
						global->admins[i]->admin.user == user)
					tmp--;
				UNLOCK(global->admins[i]);
			}
			UNLOCK(global);
			if(tmp <= 0) {
				if(sok != -1) {
					//D("close: %d",sok);
					close(sok);
				}
				if(net != NULL && nick != NULL)
					irc_send_notice(net,nick,"XDCC ADMIN Denied: You are at your logon count limit.");
				LOGP(L_XDC,"XDCC ADMIN for %s Denied: Logon limit reached.",buffer);
				return;
			}
		}
	}
	chat 			= alloc_chat(CHAT_ADMIN);
	chat->state		= STATE_QUEUED;
	chat->network 		= net;
	chat->host 		= host != NULL ? xstrdup(host) : NULL;
	chat->nick 		= nick != NULL ? xstrdup(nick) : NULL;
	chat->admin.user 	= user;
	if(sok == -1) {
		/* find a suitable chat interface */
		LOCK(global);
		for(i = 0, tmp = 0; i < global->no_interfaces && !tmp; ++i) {
			LOCK(global->interfaces[i]);
			if(global->interfaces[i]->type == INTERFACE_CHAT &&
				global->interfaces[i]->def == 1) {
				PTRARR_ADD(&global->interfaces[i]->chat.chats,
					&global->interfaces[i]->chat.no_chats,
					chat);
				tmp = 1;
			}
			UNLOCK(global->interfaces[i]);
		}
		UNLOCK(global);
		if(tmp == 0) {
			LOCK(net);
			LOGP(L_ERR,"Unable to find suitable chat interface for XDCC ADMIN to [%s:%s] will issue DCC CHAT from thread.",
				net->name,nick);
			UNLOCK(net);
		} else
			return;
	}
	admin_begin(sok,chat);
}

/** admin_purge
 * 	Frees deleted chat structures, thread joining is also done.
 */
void admin_purge(const time_t now) {
	chat_t **old = NULL;
	int i,j,deleted,no_old = 0;
	
	//D("Admin Purge Begins");
	
	LOCK(global);
	for(i = 0, deleted = 0; i < global->no_admins; ++i) {
		LOCK(global->admins[i]);
		if(global->admins[i]->state == STATE_COMPLETE)
			deleted++;
	}
	if(deleted) {
		old 		= global->admins;
		no_old 		= global->no_admins;
		PTRARR_ALLOC(&(global->admins),&(global->no_admins),
				no_old - deleted);
		for(i = 0,j = 0; i < no_old; ++i) {
			if(old[i]->state != STATE_COMPLETE) {
				UNLOCK(old[i]);
				global->admins[j++] = old[i];
				old[i] = NULL;
			}
		}
	} else {
		for(i = 0; i < global->no_admins; ++i)
			UNLOCK(global->admins[i]);
	}
	UNLOCK(global);
	if(old != NULL) {
		for(i = 0; i < no_old; ++i) {
			if(old[i] !=  NULL) {
				UNLOCK(old[i]);
				thread_end(&(old[i]->admin.thread));
				thread_join(&(old[i]->admin.thread),NULL);
				free_chat(old[i]);
			}
		}
		xfree(old);
	}

	//D("Admin Purge Ends");
}

int admin_valid(chat_t *ac) {
	int i;
	
	if(ac == NULL)
		return 0;

	LOCK(global);
	for(i = 0; i < global->no_admins; ++i) {
		if(global->admins[i] == ac) {
			LOCK(ac);
			UNLOCK(global);
			if(ac->state != STATE_COMPLETE) {
				return 1;
			} else {
				UNLOCK(ac);
				return 0;
			}
		}
	}
	UNLOCK(global);

	return 0;
}
			

/** admin_nick_change
 *	Is called to signal a nick name change on an IRC network, it updates
 *	any references in the admin structures from net:onick to net:nnick.
 */
void admin_nick_change(network_t *net, const char *onick, const char *nnick) {
	int i;

	LOCK(global);
	for(i = 0; i < global->no_admins; ++i) {
		chat_t *a = global->admins[i];
		LOCK(a);
		if(a->network == net && irc_strcasecmp(a->nick,onick) == 0) {
			xfree(a->nick);
			a->nick = xstrdup(nnick);
		}
		UNLOCK(a);
	}
	UNLOCK(global);
}

#ifdef HAVE_SYS_UN_H
/** admin_socket_open
 * 	Creates a unix listening socket on a path.
 * 	Returns -1 on failure, otherwise the new socket.
 */
int admin_socket_open(const char *path) {
	struct sockaddr_un addr;
	struct stat sbuf;
	int ret,sok;
	
	ret = stat(path,&sbuf);
	if(ret != -1) {
		if(S_ISSOCK(sbuf.st_mode)) {
			ret = unlink(path);
			if(ret == -1)
				return -1;
		} else
			return -1;
	}
	
	ret = socket(PF_UNIX,SOCK_STREAM,0);
	if(ret == -1)
		return -1;
	else
		sok = ret;
	
	addr.sun_family = AF_UNIX;
	xstrncpy(addr.sun_path,path,UNIX_PATH_MAX);
	ret = bind(sok,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1) {
		//D("close: %d",sok);
		close(sok);
		return -1;
	}
	ret = fcntl(sok,F_SETFL,O_NONBLOCK);
	if(ret == -1) {
		//D("close: %d",sok);
		close(sok);
		return -1;
	}
	listen(sok,5);
	return sok;
}

/** admin_socket_poll
 * 	Checks an admin socket for new connections, calls admin_begin on any 
 *	that occur.  Will wait sec + usec, however will return immediately if 
 * 	any new connection are made.  Returns -1 on error, 0 on success.
 */
int admin_socket_poll(int sok, long sec, long usec) {
	struct pollfd pfd;
	int ret;
	
	pfd.fd		= sok;
	pfd.events	= POLLIN;
	pfd.revents	= 0;
	
	ret = poll(&pfd,1,(sec * 1000) + (usec / 1000));
	
	if(ret < 1) {
		if(ret == -1)
			E(errno);
		if(ret == -1 && errno == EINTR)
			ret = 0;
		return ret;
	} else if(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
		return -1;
	
	do {
		ret = accept(sok,NULL,NULL);
		if(ret != -1)
			admin_begin(ret,alloc_chat(CHAT_ADMIN));
	} while(ret != -1);
	
	return 0;
}

/** admin_socket_close
 * 	shuts down an admin socket and deletes the associated
 * 	path if the path is of type socket.
 */
void admin_socket_close(const char *path, int sok) {
	struct stat sbuf;
	int ret;
	//D("close: %d",sok);
	close(sok);
	if(path != NULL) {
		ret = stat(path,&sbuf);
		if(ret != -1) {
			if(S_ISSOCK(sbuf.st_mode))
				unlink(path);
		}
	}
}
#endif /* HAVE_SYS_UN_H */

/*******************************
 * Admin Chat Thread Functions *
 *******************************/

/** has_pending_data
 *	Returns 1 is socket s has pending data, -1 if an error occured else 0.
 */
static int has_pending_data(int s) {
	struct pollfd pfd;
	int ret;
	
	//D("");
	
	pfd.fd = s;
	pfd.events = POLLIN;
	ret = poll(&pfd,1,0);
	
	if(ret == -1) {
		E(errno);
		return -1;
	}
	
	if(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
		return -1;
	else if(pfd.revents & POLLIN)
		return 1;
	else
		return 0;
}

/** recv_line
 *	Attempts to receive a m/[\r]?\n/ terminated line from the socket of an
 *	admin session, excess data is stored in the sokbuf associated with the
 *	session and resumed on subsequent calls.
 *	Returns -1 on error, 0 on no message, otherwise message length.
 */
static ssize_t recv_line(achat_t *ac, char *buffer, size_t bufsize) {
	ssize_t ret;
	int i;

	if(ac->sokpos != 0) {
		if(bufsize < ac->sokpos+1)
			return -1;
		else
			strncpy(buffer,ac->sokbuf,ac->sokpos);
	}
	
	do {
		ret = recv(ac->sok,buffer + ac->sokpos,
				bufsize - ac->sokpos,MSG_PEEK);
		if(ret < 0 && errno != EINTR) {
			E(errno);
			return -1;
		}
	} while(ret == -1 && errno == EINTR);
		
	for(i = 0; i < (ret+ac->sokpos); ++i) {
		if(buffer[i] == '\n') {
			do {
				ret = recv(ac->sok,buffer + ac->sokpos,
						(i+1) - ac->sokpos,0);
				if(ret < 0 && errno != EINTR) {
					E(errno);
					return -1;
				}
			} while(ret == -1 && errno == EINTR);
			
			buffer[i] = '\0';
			if(i != 0)
				if(buffer[i-1] == '\r')
					buffer[i-1] = '\0';
			
			ac->sokpos = 0;
			return strlen(buffer)+1;
		}
	}
	
	do {
		ret = recv(ac->sok,ac->sokbuf + ac->sokpos,ret,0);
		if(ret < 0 && errno != EINTR) {
			E(errno);
			return -1;
		}
	} while(ret == -1 && errno == EINTR);
	
	ac->sokpos += ret;
	if(ac->sokpos >= 512)
		ac->sokpos = 0; // clear buffer on oversize message
	
	E(errno);
	errno = EAGAIN;
	if(ret > 0)
		return 0;
	else
		return -1;	
}

static int wait_for_data(achat_t *ac, int timeout) {
	int i;
	for(i = 0; i < (timeout*2); ++i) {
		switch (has_pending_data(ac->sok)) {
			case -1: 
				return -1;
			case 1: 
				return (i/2);
			default:
				usleep(500000);
		}
	}
	return -1;
}

/** initiate_chat
 *	Creates a listening socket and sends a CTCP DCC CHAT message over IRC,
 *	then waits 180s for a connection.  If successful the resulting socket is
 *	assigned to the achat_t structure and 0 returned otherwise -1.
 *	This function is used when no CHAT interfaces are availible.
 */
static int initiate_chat(achat_t *ac) {
	char 			buffer[64],nick[32];
	struct pollfd		pfd;	
	struct sockaddr_in 	addr;
	socklen_t 		len;
	network_t 		*net;
	int 			i,ret;
	
	ac->sok = socket(PF_INET,SOCK_STREAM,0);
	if(ac->sok == -1)
		return -1;
	addr.sin_family = AF_INET;
	addr.sin_port	= 0;
	addr.sin_addr.s_addr = INADDR_ANY;
	ret = bind(ac->sok,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1) {
		//D("close: %d",ac->sok);
		close(ac->sok);
		return -1;
	}
	len = sizeof(addr);
	getsockname(ac->sok,(struct sockaddr *)&addr,&len);
	listen(ac->sok,1);
	
	LOCK(ac->chat);
	ac->chat->state = STATE_LISTENING;
	net = ac->chat->network;
	if(ac->chat->nick != NULL)
		xstrncpy(nick,ac->chat->nick,sizeof(nick));
	else
		nick[0] = '\0';
	UNLOCK(ac->chat);
	
	if(net == NULL) {
		//D("close: %d",ac->sok);
		close(ac->sok);
		errno = ENETUNREACH;
		return -1;
	}
	
	snprintf(buffer,sizeof(buffer),
			"DCC CHAT CHAT $IIP %u",
			ntohs(addr.sin_port));
	irc_send_ctcp_msg(net,nick,buffer);
	LOCK(net);
	LOGTP(L_INF,"Dispatch DCC CHAT message to [%s:%s]",
			net->name,nick);
	UNLOCK(net);
	
	pfd.fd		= ac->sok;
	pfd.events	= POLLIN | POLLOUT;
	pfd.revents	= 0;
	
	for(i = 0; i < 180; ++i) {
		ret = poll(&pfd,1,1000);
		if(ret != 0) {
			break;
		} else if(thread_should_end()) {
			errno = EINTR;
			ret = -1;
			break;
		}
	}
	
	if(ret < 1 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) {
		i = errno;
		//D("close: %d",ac->sok);
		close(ac->sok);
		errno = i;
		if(ret == 0)
			errno = ETIMEDOUT;
		return -1;
	}
	ret = accept(ac->sok,NULL,NULL);
	if(ret == -1) {
		i = errno;
		//D("close: %d",ac->sok);
		close(ac->sok);
		errno = i;
		return -1;
	}
	//D("close: %d",ac->sok);
	close(ac->sok);
	ac->sok = ret;
	return 0;
}

/** authenticate_chat
 *	Authenticates, the user against their their password.
 *	Determines the remote user if not already know.
 *	Checks that the remote hostname matches one of the users permitted
 *	hostmasks.  Hardcoded retry limits 1 user retry, 1 password retry.
 *	Returns -1 on authentication failure and socket errors, 0 on success.
 */
static int authenticate_chat(achat_t *ac) {
	char			buffer[512];
	struct sockaddr_in	saddr;
	struct sockaddr_in	*addr;
	socklen_t		len;
	int i,j,ret;
	
#ifdef HAVE_SYS_UN_H
	len = IMAX(sizeof(struct sockaddr_in),sizeof(struct sockaddr_un));
#else
	len = sizeof(struct sockaddr_in);
#endif
	addr = xalloc(len);
	ret = getsockname(ac->sok,(struct sockaddr *)addr,&len);
	if(addr->sin_family == AF_INET) {
		len = sizeof(struct sockaddr_in);
		ret = getpeername(ac->sok,(struct sockaddr *)addr,&len);
		if(ret == -1) {
			LOGTP(L_ERR,"Failed to get peer name");
			xfree(addr);
			return -1;
		}
		memcpy(&saddr,addr,sizeof(struct sockaddr_in));
		inet_ntop(AF_INET,&addr->sin_addr,buffer,sizeof(buffer));
		LOGTP(L_INF,"Connected to (%s:%u)",buffer,ntohs(addr->sin_port));
#ifndef HAVE_SYS_UN_H
	}
#else
	} else {
		LOGTP(L_INF,"Connected via Admin Socket");
		saddr.sin_family = AF_UNIX;
	}
#endif /* HAVE_SYS_UN_H */
	xfree(addr);
	
	if(ac->user == NULL) {
		for(j = 0; j < 2; ++j) {
			i = snprintf(buffer,sizeof(buffer),"Username:\r\n");
			if((ret = send(ac->sok,buffer,i,0)) == -1)
				return ret;
			if((ret = wait_for_data(ac,180)) == -1)
				return ret;
			do {
				if((ret = recv_line(ac,buffer,sizeof(buffer))) == -1)
					return ret;
			} while(ret == 0);
			//D("buffer: %s",buffer);
			ac->user = user_find(buffer);
			if(ac->user != NULL)
				break;
		}
		if(j >= 2) {
			LOGTP(L_ERR,"Authentication Failure: Retry Limit Exceeded (Username)");
			i = snprintf(buffer,sizeof(buffer),"Retry Limit Exceeded\r\n");
			send(ac->sok,buffer,i,0);
			return -1;
		}
	}
	
	LOCK(ac->user);
	LOGTP(L_INF,"Username: %s",ac->user->username);
	if(ac->user->no_permitted_logons != 0) {
		j = ac->user->no_permitted_logons;
		UNLOCK(ac->user);
		LOCK(global);
		for(i = 0; i < global->no_admins && j > 0; ++i) {
			LOCK(global->admins[i]);
			if(global->admins[i] != ac->chat &&
				global->admins[i]->state != STATE_COMPLETE &&
				global->admins[i]->state != STATE_DELETED &&
				global->admins[i]->admin.user == ac->user)
				j--;
			UNLOCK(global->admins[i]);
		}
		UNLOCK(global);
		if(j <= 0) {
			LOGTP(L_ERR,"Authentication Failure: Logon count exceeded");
			i = snprintf(buffer,sizeof(buffer),
					"Logon count exceeded\r\n");
			send(ac->sok,buffer,i,0);
			return -1;
		}
	} else
		UNLOCK(ac->user);
	
	if(saddr.sin_family == AF_INET) {
		char **names;
		int no_names;
		
		ret = aresolv_addrtonames(global->aresolver,AF_INET,
				&(saddr.sin_addr),&names);
		//D("host: %s",buffer);
		if(ret < 1) {
			char addrbuf[64];
			LOGTP(L_ERR,"Unable to determine remote hostname, using IP to test access list");
			
			inet_ntop(AF_INET,&(saddr.sin_addr),
					addrbuf,sizeof(addrbuf));
			names = xalloc(sizeof(char *) + strlen(addrbuf) + 1);
			names[0] = (char *)&(names[1]);
			strcpy(names[0], addrbuf);
			no_names = 1;
		} else {
			no_names = ret;
		}
		
		LOCK(ac->user);
		for(i = 0, ret = 0; i < ac->user->no_hosts && !ret; ++i) {
			for(j = 0; j < no_names; ++j) {
				if(irc_hostmask_match(names[j],ac->user->hosts[i]))
					ret = 1;
			}
		}
		UNLOCK(ac->user);
		
		xfree(names);
		if(ret == 0) {
			LOGTP(L_ERR,"Authentication Failure: Host not authorized");
			i = snprintf(buffer,sizeof(buffer),
					"Your host is not authorized\r\n");
			send(ac->sok,buffer,i,0);
			return -1;
		}
	} else { /* we don't need to #ifdef this part */
		LOCK(ac->user);
		if(!PRIV_ISSET(ac->user,PRIV_SOCKET)) {
			UNLOCK(ac->user);
			LOGTP(L_ERR,"Authentication Failure: User not granted Admin Socket privilege");
			i = snprintf(buffer,sizeof(buffer),
					"You are not authorized to use this socket\r\n");
			send(ac->sok,buffer,i,0);
			return -1;
		} else
			UNLOCK(ac->user);
	}
	for(i = 0; i < 2; ++i) {
		j = snprintf(buffer,sizeof(buffer),"Password:\r\n");
		if((ret = send(ac->sok,buffer,j,0)) == -1)
			return ret;
		if((ret = wait_for_data(ac,180)) == -1)
			return ret;
		do {
			if((ret = recv_line(ac,buffer,sizeof(buffer))) == -1)
				return ret;
		} while(ret == 0);
		//D("ret: %d, buffer: \"%s\"",ret,buffer);
		
		if(user_check_password(ac->user,buffer)) {
			memset(buffer,0,sizeof(buffer));
			break;
		} else
			memset(buffer,0,sizeof(buffer));
	}
	if(i >= 2) {
		LOGTP(L_ERR,"Authentication Failure: Retry Limit Exceeded (Password)");
		i = snprintf(buffer,sizeof(buffer),"Retry Limit Exceeded\r\n");
		send(ac->sok,buffer,i,0);
		return -1;
	}
	LOCK(ac->user);
	ac->user->last_logon = xtime();
	LOGTP(L_INF,"Successfully Authenticated Username: %s",ac->user->username);
	LOG_TITLE("Admin %s",ac->user->username);
	UNLOCK(ac->user);
	LOCK(ac->chat);
	ac->chat->admin.user = ac->user;
	UNLOCK(ac->chat);
	ac->context->user = ac->user;
	se_notify_admin_logon(ac->chat,ac->user);
	return 0;
}

/** admin_send_msgs
 *	Sends out all the message queue, returns -1 if a socket error occurs
 *	otherwise 0.  Messace colour coding is done here.
 */
static int admin_send_msgs(achat_t *ac) {
	char cbuffer[510],buffer[514],*ptr;
	int len,ret;
	
	while((ptr = pqueue_pop_front(ac->msgqueue)) != NULL) {
		//LOG("Admin Thread (%p), Sending: %s",ac,ptr);
		colourise(ac->context->colour,cbuffer,sizeof(cbuffer),ptr);
		xfree(ptr);
		len = IMIN(
			snprintf(buffer,sizeof(buffer)-1,"%s\r\n",cbuffer),
			sizeof(buffer)-2
		);
		ret = send(ac->sok,buffer,len,0);
		if(ret < len) {
			//D("ret: %d, len: %d",ret,len);
			return -1;
		}
	}

	return 0;	
}

/** admin_recv_msgs
 *	Recieves messages one at a time using recv_line, calls process_cmd on 
 *	them, until there are no more pending messages.
 *	Returns -1 on socket error else 0.
 */
static int admin_recv_msgs(achat_t *ac) {
	char buffer[512];
	int ret;
	
	do {
		ret = recv_line(ac,buffer,sizeof(buffer));
		if(ret > 0) {
			LOCK(ac->chat);
			ac->chat->last_contact = xtime();
			UNLOCK(ac->chat);
			//LOG("Admin Thread (%p), Received: %s\n",ac,buffer);
			admin_process_cmd(ac->context,buffer,sizeof(buffer));
			memset(buffer,0,sizeof(buffer));
		} else if(ret < 0) {
			E(errno);
			break;
		}
	} while(has_pending_data(ac->sok) > 0);
	
	return ret;
}

/** admin_thread
 *	Main loop used by any admin session.  Calls other functions to initiate
 *	and authenticate the admin session.
 *	Uses a blocking socket, and poll call with a 15 second time out,
 *	hence the inner loop will generally iterate once every 15 seconds.
 *	On each iteration, the state of the session is check to make sure it
 *	hasn't been closed, and that the user hasn't been deleted.
 *	Every 60 seconds the admin_check function is also called.
 *	On exiting the loop all deinitialization is done except for 
 *	de-allocation of the chat_t structure, which is left to the admin_purge
 *	function so that thread joining can take place.
 */
static void *admin_thread(void *arg) {
#define SIG 1
#define SOK 0
	achat_t 		*ac = (achat_t *)arg;
	struct pollfd		pfd[2];
	int			signal_fd[2] = {-1,-1};
	int 			ret,i;
	time_t			now,last_check;
	
	thread_signal_started(&(ac->chat->admin.thread));
	
	LOG_TITLE("Admin (%p)",ac->chat);
	
	LOCK(ac->chat);
	ac->chat->started = xtime();
	if(ac->sok == -1 && 
		(ac->chat->network == NULL || ac->chat->nick == NULL)) {
		UNLOCK(ac->chat);
		goto out;
	}
	UNLOCK(ac->chat);
	
	if(ac->sok == -1) {
		ret = initiate_chat(ac);
		if(ret == -1) {
			char errbuf[96];
			LOGTP(L_ERR,"Error Initiating Chat: %s",
				lstrerror_r(errno,errbuf,sizeof(errbuf)));
			goto out;
		}
	}
	
	/* Make sure the socket IS blocking, socket which come from the
	 * interface module will be set nonblocking.
	 */
	ret = fcntl(ac->sok,F_GETFL); 
	if(ret & O_NONBLOCK) {
		ret = ret & ~O_NONBLOCK;
		fcntl(ac->sok,F_SETFL,ret);
	}

	ret = authenticate_chat(ac);
	if(ret == -1)
		goto out;
	else {
		i = 1;
		setsockopt(ac->sok,SOL_SOCKET,SO_KEEPALIVE,&i,sizeof(i));
	}
	
	LOCK(ac->chat);
	ac->chat->state		= STATE_ACTIVE;
	ac->chat->connected	= xtime();
	ac->chat->last_contact	= xtime();
	UNLOCK(ac->chat);
	
	pfd[SOK].fd	= ac->sok;
	pfd[SOK].events	= POLLIN;
	pfd[SIG].fd	= -1;
	pfd[SIG].events = POLLIN;
	
	ret = pipe(signal_fd);
	if(ret != 0) {
		LOGTP(L_ERR,"Failed to allocate signaling pipe");
		signal_fd[0] = -1;
	} else {
		fcntl(signal_fd[0],F_SETFL,O_NONBLOCK);
		fcntl(signal_fd[1],F_SETFL,O_NONBLOCK);
		pqueue_set_notification_fd(ac->msgqueue,signal_fd[1]);
		pfd[SIG].fd = signal_fd[0];
	}
	
	admin_output_prompt(ac->context);
	for(last_check = xtime();;) {
		now = xtime();
		LOCK(ac->chat);
		if(ac->chat->state == STATE_DELETED || 
			ac->chat->state == STATE_COMPLETE) {
			UNLOCK(ac->chat);
			break;
		}
		UNLOCK(ac->chat);
		LOCK(ac->user);
		if(ac->user->deleted != 0) {
			LOGTP(L_ERR,"User: %s was deleted",ac->user->username);
			UNLOCK(ac->user);
			break;
		}
		UNLOCK(ac->user);
		if((now - last_check) > 60) {
			admin_context_check(ac->context,now);
			last_check = now;
		}
		if(admin_context_complete(ac->context))
			break;
		if(pqueue_length(ac->msgqueue) == 0) {
			pfd[SIG].revents = 0;
			pfd[SOK].revents = 0;
			ret = poll(pfd,signal_fd[0] == -1 ? 1 : 2, 15*1000);
			if(ret > 0) {
				if(pfd[SOK].revents & 
						(POLLERR | POLLHUP | POLLNVAL))
					ret = -1;
				else if(pfd[SOK].revents & POLLIN)
					ret = admin_recv_msgs(ac);
				else if(pfd[SIG].revents & POLLIN) {
					do {
						char buffer[4];
						ret = read(signal_fd[0],buffer,1);
					} while(ret > 0);
					ret = 0;
				}
			}
		} else
			ret = admin_send_msgs(ac);
		if(ret == -1) {
			char errbuf[96];
			if(errno == 0) {
				socklen_t len;
				int val;
				len = sizeof(val);
				ret = getsockopt(ac->sok,SOL_SOCKET,SO_ERROR,&val,&len);
				errno = ret != -1 ? val : errno;
			}
			LOGTP(L_ERR,"Error: %s",
				lstrerror_r(errno,errbuf,sizeof(errbuf)));
			break;
		}
	}
	
out:
	log_unsubscribe(ac->msgqueue);
	irc_unlisten(NULL,NULL,ac->msgqueue);
	admin_party_line_unsubscribe(ac->context);

	if(ac->context->user != NULL)
		se_notify_admin_logoff(ac->context->user);
	admin_free_context(ac->context);
	
	pqueue_clear(ac->msgqueue,xfree,1);
	if(signal_fd[0] != -1) {
		//D("close: %d",signal_fd[0]);
		close(signal_fd[0]);
	}

	if(ac->sok != -1) {
		send(ac->sok,"Ja ne!\r\n",8,0);
		//D("close: %d",ac->sok);
		close(ac->sok);
	}
	
	LOCK(ac->chat);
	ac->chat->state = STATE_COMPLETE;
	UNLOCK(ac->chat);
	
	LOGTP(L_INF,"Shutdown");
	xfree(ac->sokbuf);
	xfree(ac);

	thread_signal_finished();
	
	return NULL;
#undef SIG
#undef SOK
}
