/* This file is part of the Project Athena Zephyr Notification System.
 * It contains the hostmanager <--> server interaction routines.
 *
 *      Created by:     David C. Jedlinsky
 *
 *      $Id: 4c584287e79ae70ae9f1985e3496581896d1c8e0 $
 *
 *      Copyright (c) 1987 by the Massachusetts Institute of Technology.
 *      For copying and distribution information, see the file
 *      "mit-copyright.h".
 */

#include "zhm.h"

#ifndef lint
#ifndef SABER
static const char rcsid_hm_server_c[] = "$Id: 4c584287e79ae70ae9f1985e3496581896d1c8e0 $";
#endif /* SABER */
#endif /* lint */

static void boot_timeout __P((void *));
static int get_serv_timeout __P((void));

static Timer *boot_timer = NULL;
static int serv_rexmit_times[] = { 5, 10, 20, 40 };
static int serv_timeouts = 0;

int serv_loop = 0;

void hm_control(ZNotice_t *);
void send_back(ZNotice_t *);

/* Argument is whether we are actually booting, or just attaching
 * after a server switch */
void
send_boot_notice(char *op)
{
     ZNotice_t notice;
     Code_t ret;

     memset(&notice, 0, sizeof(ZNotice_t));
     /* Set up server notice */
     notice.z_kind = HMCTL;
     notice.z_port = cli_port;
     notice.z_class = ZEPHYR_CTL_CLASS;
     notice.z_class_inst = ZEPHYR_CTL_HM;
     notice.z_opcode = op;
     notice.z_sender = "HM";
     notice.z_recipient = "";
     notice.z_default_format = "";
     notice.z_num_other_fields = 0;
     notice.z_message_len = 0;

     /* Notify server that this host is here */
     if ((ret = ZSetDestAddr(&serv_sin)) != ZERR_NONE) {
	  Zperr(ret);
	  com_err("hm", ret, "setting destination");
     }
     if ((ret = ZSendNotice(&notice, ZNOAUTH)) != ZERR_NONE) {
	  Zperr(ret);
	  com_err("hm", ret, "sending startup notice");
     }
     boot_timer = timer_set_rel(get_serv_timeout(), boot_timeout, NULL);
}

/* Argument is whether we are detaching or really going down */
void
send_flush_notice(char *op)
{
     ZNotice_t notice;
     Code_t ret;

     memset(&notice, 0, sizeof(ZNotice_t));
     /* Set up server notice */
     notice.z_kind = HMCTL;
     notice.z_port = cli_port;
     notice.z_class = ZEPHYR_CTL_CLASS;
     notice.z_class_inst = ZEPHYR_CTL_HM;
     notice.z_opcode = op;
     notice.z_sender = "HM";
     notice.z_recipient = "";
     notice.z_default_format = "";
     notice.z_num_other_fields = 0;
     notice.z_message_len = 0;

     /* Tell server to lose us */
     if ((ret = ZSetDestAddr(&serv_sin)) != ZERR_NONE) {
	  Zperr(ret);
	  com_err("hm", ret, "setting destination");
     }
     if ((ret = ZSendNotice(&notice, ZNOAUTH)) != ZERR_NONE) {
	  Zperr(ret);
	  com_err("hm", ret, "sending flush notice");
     }
}

void
find_next_server(char *sugg_serv)
{
     struct hostent *hp = 0;
     int done = 0;
     char **parse = serv_list;
     char *new_serv;

     if (sugg_serv) {
	  do {
	       if (!strcmp(*parse, sugg_serv))
		    done = 1;
	  } while ((done == 0) && (*++parse != NULL));
     }
     if (done) {
	  if ((hp = gethostbyname(sugg_serv)) != NULL) {
	       DPR2 ("Server = %s\n", sugg_serv);
	       (void)strncpy(cur_serv, sugg_serv, NS_MAXDNAME);
	       cur_serv[NS_MAXDNAME - 1] = '\0';
	       if (hmdebug)
		    syslog(LOG_DEBUG, "Suggested server: %s\n", sugg_serv);
	  } else {
	       done = 0;
	  }
     }
     while (!done) {
	 if ((++serv_loop > 3) && (strcmp(cur_serv, prim_serv))) {
	     serv_loop = 0;
	     if ((hp = gethostbyname(prim_serv)) != NULL) {
		 DPR2 ("Server = %s\n", prim_serv);
		 (void)strncpy(cur_serv, prim_serv, NS_MAXDNAME);
		 cur_serv[NS_MAXDNAME - 1] = '\0';
		 done = 1;
		 break;
	     }
	 }

	 switch (numserv) {
	 case 1:
	     if ((hp = gethostbyname(*serv_list)) != NULL) {
		 DPR2 ("Server = %s\n", *serv_list);
		 (void)strncpy(cur_serv, *serv_list, NS_MAXDNAME);
		 cur_serv[NS_MAXDNAME - 1] = '\0';
		 done = 1;
		 break;
	     }
	     /* fall through */
	 case 0:
	     if (rebootflag)
		 die_gracefully();
	     else
		 sleep(1);
	     break;
	 default:
	     do {
		 new_serv = serv_list[random() % numserv];
	     } while (!strcmp(new_serv, cur_serv));

	     if ((hp = gethostbyname(new_serv)) != NULL) {
		 DPR2 ("Server = %s\n", new_serv);
		 (void)strncpy(cur_serv, new_serv, NS_MAXDNAME);
		 cur_serv[NS_MAXDNAME - 1] = '\0';
		 done = 1;
	     } else
		 sleep(1);

	     break;
	 }
     }
     (void) memcpy((char *)&serv_sin.sin_addr, hp->h_addr, 4);
     nservchang++;
}

void
server_manager(ZNotice_t *notice)
{
    if (memcmp((char *)&serv_sin.sin_addr, (char *)&from.sin_addr, 4) ||
	(serv_sin.sin_port != from.sin_port)) {
	syslog (LOG_INFO, "Bad notice from port %u.", notice->z_port);
    } else {
	/* This is our server, handle the notice */
	booting = 0;
	serv_timeouts = 0;
	if (boot_timer) {
	    timer_reset(boot_timer);
	    boot_timer = NULL;
	}
	DPR ("A notice came in from the server.\n");
	nserv++;
	switch(notice->z_kind) {
	case HMCTL:
	    hm_control(notice);
	    break;
	case SERVNAK:
	case SERVACK:
	    send_back(notice);
	    break;
	default:
	    syslog (LOG_INFO, "Bad notice kind!?");
	    break;
	}
    }
}

void
hm_control(ZNotice_t *notice)
{
    Code_t ret;
    struct hostent *hp;
    char suggested_server[NS_MAXDNAME];
    unsigned long addr;

    DPR("Control message!\n");
    if (!strcmp(notice->z_opcode, SERVER_SHUTDOWN)) {
	if (notice->z_message_len) {
	    addr = inet_addr(notice->z_message);
	    hp = gethostbyaddr((char *) &addr, sizeof(addr), AF_INET);
	    if (hp != NULL) {
		strncpy(suggested_server, hp->h_name, sizeof(suggested_server));
		suggested_server[sizeof(suggested_server) - 1] = '\0';
		new_server(suggested_server);
	    } else {
		new_server(NULL);
	    }
	} else {
	    new_server((char *)NULL);
	}
    } else if (!strcmp(notice->z_opcode, SERVER_PING)) {
	notice->z_kind = HMACK;
	if ((ret = ZSetDestAddr(&serv_sin)) != ZERR_NONE) {
	    Zperr(ret);
	    com_err("hm", ret, "setting destination");
	}
	if ((ret = send_outgoing(notice)) != ZERR_NONE) {
	    Zperr(ret);
	    com_err("hm", ret, "sending ACK");
	}
	if (no_server) {
	    no_server = 0;
	    retransmit_queue(&serv_sin);
	}
    } else {
	syslog (LOG_INFO, "Bad control message.");
    }
}

void
send_back(ZNotice_t *notice)
{
    ZNotice_Kind_t kind;
    struct sockaddr_in repl;
    Code_t ret;

    if (!strcmp(notice->z_opcode, HM_BOOT) ||
	!strcmp(notice->z_opcode, HM_ATTACH)) {
	/* ignore message, just an ack from boot, but exit if we
	 * are rebooting.
	 */
	if (rebootflag)
	    die_gracefully();
    } else {
	if (remove_notice_from_queue(notice, &kind, &repl) != ZERR_NONE) {
	    syslog (LOG_INFO, "Hey! This packet isn't in my queue!");
	} else {
	    /* check if client wants an ACK, and send it */
	    if (kind == ACKED) {
		DPR2 ("Client ACK port: %u\n", ntohs(repl.sin_port));
		if ((ret = ZSetDestAddr(&repl)) != ZERR_NONE) {
		    Zperr(ret);
		    com_err("hm", ret, "setting destination");
		}
		if ((ret = send_outgoing(notice)) != ZERR_NONE) {
		    Zperr(ret);
		    com_err("hm", ret, "sending ACK");
		}
	    }
	}
    }
    if (no_server) {
	no_server = 0;
	retransmit_queue(&serv_sin);
    }
}

void
new_server(char *sugg_serv)
{
    no_server = 1;
    syslog (LOG_INFO, "Server went down, finding new server.");
    send_flush_notice(HM_DETACH);
    find_next_server(sugg_serv);
    if (booting || deactivated) {
	send_boot_notice(HM_BOOT);
	deactivated = 0;
    } else {
	send_boot_notice(HM_ATTACH);
    }
    disable_queue_retransmits();
}

static void
boot_timeout(void *arg)
{
    serv_timeouts++;
    new_server(NULL);
}

static int get_serv_timeout(void)
{
    int ind, ntimeouts;

    ind = (numserv == 0) ? serv_timeouts : serv_timeouts / numserv;
    ntimeouts = sizeof(serv_rexmit_times) / sizeof(*serv_rexmit_times);
    if (ind >= ntimeouts)
	ind = ntimeouts - 1;
    return serv_rexmit_times[ind];
}
