/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <arpa/inet.h>
#include <errno.h>
#include <inet/ip.h>
#include <libdladm.h>
#include <libdllink.h>
#include <libdlwlan.h>
#include <libscf.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <libnwam.h>
#include "conditions.h"
#include "events.h"
#include "objects.h"
#include "util.h"

/*
 * loc.c - contains routines which handle location abstraction.
 */

pthread_mutex_t active_loc_mutex = PTHREAD_MUTEX_INITIALIZER;
char active_loc[NWAM_MAX_NAME_LEN];

static int
loc_create_init_fini_event(nwam_loc_handle_t loch, void *data)
{
	boolean_t *init = data;
	char *name;
	nwamd_event_t event;

	if (nwam_loc_get_name(loch, &name) != NWAM_SUCCESS) {
		nlog(LOG_ERR, "loc_init_fini: could not get loc name");
		return (0);
	}

	event = nwamd_event_init(*init ?
	    NWAM_EVENT_TYPE_OBJECT_INIT : NWAM_EVENT_TYPE_OBJECT_FINI,
	    NWAM_OBJECT_TYPE_LOC, 0, name);
	if (event != NULL)
		nwamd_event_enqueue(event);
	free(name);

	return (0);
}

/*
 * Walk all locs, creating init events for each.
 */
void
nwamd_init_locs(void)
{
	boolean_t init = B_TRUE;

	/* Unset active location */
	(void) pthread_mutex_lock(&active_loc_mutex);
	active_loc[0] = '\0';
	(void) pthread_mutex_unlock(&active_loc_mutex);
	(void) nwam_walk_locs(loc_create_init_fini_event, &init, 0, NULL);
}

/*
 * Walk all locs, creating fini events for each.
 */
void
nwamd_fini_locs(void)
{
	boolean_t init = B_FALSE;

	(void) nwam_walk_locs(loc_create_init_fini_event, &init, 0, NULL);
}

static boolean_t
loc_is_enabled(nwam_loc_handle_t loch)
{
	nwam_value_t enabledval;
	boolean_t enabled = B_FALSE;

	if (nwam_loc_get_prop_value(loch, NWAM_LOC_PROP_ENABLED,
	    &enabledval) != NWAM_SUCCESS) {
		nlog(LOG_ERR, "loc_is_enabled: could not retrieve "
		    "enabled value");
		return (B_FALSE);
	}
	if (nwam_value_get_boolean(enabledval, &enabled)
	    != NWAM_SUCCESS) {
		nlog(LOG_ERR, "loc_is_enabled: could not retrieve "
		    "enabled value");
		nwam_value_free(enabledval);
		return (B_FALSE);
	}
	nwam_value_free(enabledval);
	return (enabled);
}

static int64_t
loc_get_activation_mode(nwam_loc_handle_t loch)
{
	nwam_error_t err;
	uint64_t activation;
	nwam_value_t activationval;

	if (nwam_loc_get_prop_value(loch, NWAM_LOC_PROP_ACTIVATION_MODE,
	    &activationval)  != NWAM_SUCCESS) {
		nlog(LOG_ERR, "loc_get_activation_mode: could not retrieve "
		    "activation mode value");
		return (-1);
	}
	err = nwam_value_get_uint64(activationval, &activation);
	nwam_value_free(activationval);
	if (err != NWAM_SUCCESS) {
		nlog(LOG_ERR, "loc_get_activation_mode: could not retrieve "
		    "activation mode value");
		return (-1);
	}

	return ((int64_t)activation);
}

/* Enables the location. */
static void
nwamd_loc_activate(const char *object_name)
{
	char *enabled;

	nlog(LOG_DEBUG, "nwamd_loc_activate: activating loc %s",
	    object_name);

	/*
	 * Find currently enabled location and change its state to disabled
	 * if it is a manual location, or offline (if it is not).
	 * Only manual locations reach disabled, since conditional and
	 * system locations which are manually disabled simply revert to
	 * their conditions for activation.
	 */
	if ((enabled = malloc(NWAM_MAX_NAME_LEN)) != NULL &&
	    nwamd_lookup_string_property(NET_LOC_FMRI, NET_LOC_PG,
	    NET_LOC_SELECTED_PROP, enabled, NWAM_MAX_NAME_LEN) == 0) {
		/* Only change state if current != new */
		if (strcmp(enabled, object_name) != 0) {
			boolean_t do_disable = B_FALSE;
			nwamd_object_t eobj = nwamd_object_find
			    (NWAM_OBJECT_TYPE_LOC, enabled);
			if (eobj == NULL) {
				nlog(LOG_INFO, "nwamd_loc_activate: could not "
				    "find old location %s", enabled);
				goto skip_disable;
			}
			/*
			 * Disable if the old location was manual, since the
			 * only way a manual location can deactivate is if
			 * it is disabled.
			 */
			do_disable =
			    (loc_get_activation_mode(eobj->nwamd_object_handle)
			    == (int64_t)NWAM_ACTIVATION_MODE_MANUAL);
			nwamd_object_release(eobj);

			if (do_disable) {
				nlog(LOG_DEBUG, "nwamd_loc_activate: "
				    "disable needed for old location %s",
				    enabled);
				nwamd_object_set_state
				    (NWAM_OBJECT_TYPE_LOC, enabled,
				    NWAM_STATE_DISABLED,
				    NWAM_AUX_STATE_MANUAL_DISABLE);
			} else {
				nlog(LOG_DEBUG, "nwamd_loc_activate: "
				    "offline needed for old location %s",
				    enabled);
				nwamd_object_set_state
				    (NWAM_OBJECT_TYPE_LOC, enabled,
				    NWAM_STATE_OFFLINE,
				    NWAM_AUX_STATE_CONDITIONS_NOT_MET);
			}
		}
	}
skip_disable:
	free(enabled);

	if (nwamd_set_string_property(NET_LOC_FMRI, NET_LOC_PG,
	    NET_LOC_SELECTED_PROP, object_name) == 0) {
		char *state = smf_get_state(NET_LOC_FMRI);
		nlog(LOG_INFO, "nwamd_loc_activate: set %s/%s to %s; "
		    "service is in %s state", NET_LOC_PG, NET_LOC_SELECTED_PROP,
		    object_name, state == NULL ? "unknown" : state);
		free(state);
		(void) smf_restore_instance(NET_LOC_FMRI);
		if (smf_refresh_instance(NET_LOC_FMRI) == 0) {
			(void) pthread_mutex_lock(&active_loc_mutex);
			(void) strlcpy(active_loc, object_name,
			    sizeof (active_loc));
			(void) pthread_mutex_unlock(&active_loc_mutex);
			nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
			    object_name,
			    NWAM_STATE_ONLINE, NWAM_AUX_STATE_ACTIVE);
		} else {
			nlog(LOG_ERR, "nwamd_loc_activate: "
			    "%s could not be refreshed", NET_LOC_FMRI);
			nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
			    object_name,
			    NWAM_STATE_MAINTENANCE,
			    NWAM_AUX_STATE_METHOD_FAILED);
		}
	}
}

struct nwamd_loc_check_walk_arg {
	nwamd_object_t winning_object;
	uint64_t winning_rating;
};

/*
 * Determine which location should be activated.
 */
static int
nwamd_loc_check(nwamd_object_t object, void *data)
{
	struct nwamd_loc_check_walk_arg *wa = data;
	nwam_loc_handle_t loch = object->nwamd_object_handle;
	nwam_value_t conditionval;
	int64_t lactivation;
	uint64_t rating, activation;
	boolean_t satisfied;
	char **conditions;
	uint_t nelem;

	lactivation = loc_get_activation_mode(object->nwamd_object_handle);

	if (lactivation == -1)
		return (0);

	activation = (uint64_t)lactivation;
	switch (activation) {
	case NWAM_ACTIVATION_MODE_MANUAL:
		if (loc_is_enabled(loch)) {
			/* Manually enabled locations should always win out. */
			nlog(LOG_DEBUG, "nwamd_loc_check: %s is enabled",
			    object->nwamd_object_name);
			wa->winning_object = object;
			wa->winning_rating = UINT64_MAX;
		} else {
			nlog(LOG_DEBUG, "nwamd_loc_check: %s is disabled",
			    object->nwamd_object_name);
			if (object->nwamd_object_state != NWAM_STATE_DISABLED) {
				nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
				    object->nwamd_object_name,
				    NWAM_STATE_DISABLED,
				    NWAM_AUX_STATE_MANUAL_DISABLE);
			}
		}

		return (0);

	case NWAM_ACTIVATION_MODE_CONDITIONAL_ANY:
	case NWAM_ACTIVATION_MODE_CONDITIONAL_ALL:
		if (loc_is_enabled(loch)) {
			/* Manually enabled locations should always win out. */
			nlog(LOG_DEBUG, "nwamd_loc_check: %s is enabled",
			    object->nwamd_object_name);
			wa->winning_object = object;
			wa->winning_rating = UINT64_MAX;
		}

		if (nwam_loc_get_prop_value(loch,
		    NWAM_LOC_PROP_CONDITIONS, &conditionval) != NWAM_SUCCESS) {
			nlog(LOG_ERR, "nwamd_loc_check: could not retrieve "
			    "condition value");
			return (0);
		}
		if (nwam_value_get_string_array(conditionval,
		    &conditions, &nelem) != NWAM_SUCCESS) {
			nlog(LOG_ERR, "nwamd_loc_check: could not retrieve "
			    "condition value");
			nwam_value_free(conditionval);
			return (0);
		}
		satisfied = nwamd_check_conditions(activation, conditions,
		    nelem);

		if (satisfied) {
			rating = nwamd_rate_conditions(activation,
			    conditions, nelem);
			if (rating > wa->winning_rating) {
				wa->winning_object = object;
				wa->winning_rating = rating;
			}
		}
		nwam_value_free(conditionval);
		return (0);

	case NWAM_ACTIVATION_MODE_SYSTEM:
		if (loc_is_enabled(loch)) {
			/* Manually enabled locations should always win out. */
			nlog(LOG_DEBUG, "nwamd_loc_check: %s is enabled",
			    object->nwamd_object_name);
			wa->winning_object = object;
			wa->winning_rating = UINT64_MAX;
		}

		/* Either NoNet, Automatic or Legacy location, so skip. */

		return (0);
	default:
		return (0);
	}
	/*NOTREACHED*/
	return (0);
}

static int
nwamd_ncu_online_check(nwamd_object_t object, void *data)
{
	boolean_t *online = data;
	nwamd_ncu_t *ncu_data = object->nwamd_object_data;

	if (ncu_data->ncu_type != NWAM_NCU_TYPE_INTERFACE)
		return (0);

	if (object->nwamd_object_state == NWAM_STATE_ONLINE) {
		/* An online IP NCU found, stop walk */
		*online = B_TRUE;
		return (1);
	}
	return (0);
}

void
nwamd_loc_check_conditions(void)
{
	struct nwamd_loc_check_walk_arg wa = { NULL, 0 };
	const char *winning_loc;
	boolean_t ncu_online = B_FALSE;
	boolean_t is_active;

	/*
	 * Walk the NCUs to find out if at least one IP NCU is online.  If so,
	 * check the activation-mode and conditions.  If not, enable the NoNet
	 * location.
	 */
	(void) nwamd_walk_objects(NWAM_OBJECT_TYPE_NCU, nwamd_ncu_online_check,
	    &ncu_online);

	if (!ncu_online) {
		winning_loc = NWAM_LOC_NAME_NO_NET;
	} else {
		(void) nwamd_walk_objects(NWAM_OBJECT_TYPE_LOC, nwamd_loc_check,
		    &wa);
		if (wa.winning_object != NULL)
			winning_loc = wa.winning_object->nwamd_object_name;
		else
			winning_loc = NWAM_LOC_NAME_AUTOMATIC;
	}
	nlog(LOG_DEBUG, "nwamd_loc_check_conditions: winning loc is %s",
	    winning_loc);

	/* If the winning location is already active, do nothing */
	(void) pthread_mutex_lock(&active_loc_mutex);
	is_active = (strcmp(active_loc, winning_loc) == 0);
	(void) pthread_mutex_unlock(&active_loc_mutex);
	if (is_active)
		return;

	nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC, winning_loc,
	    NWAM_STATE_OFFLINE_TO_ONLINE, NWAM_AUX_STATE_METHOD_RUNNING);
}

int
nwamd_loc_action(const char *loc, nwam_action_t action)
{
	nwamd_event_t event = nwamd_event_init_object_action
	    (NWAM_OBJECT_TYPE_LOC, loc, NULL, action);
	if (event == NULL)
		return (1);
	nwamd_event_enqueue(event);
	return (0);
}

/*
 * Event handling functions.
 */

/* Handle loc initialization/refresh event */
void
nwamd_loc_handle_init_event(nwamd_event_t event)
{
	nwamd_object_t object;
	nwam_loc_handle_t loch;
	nwam_error_t err;
	boolean_t new_enabled, old_enabled = B_FALSE;
	nwam_state_t state;

	if ((err = nwam_loc_read(event->event_object, 0, &loch))
	    != NWAM_SUCCESS) {
		nlog(LOG_ERR, "nwamd_loc_handle_init_event: could not "
		    "read object '%s': %s", event->event_object,
		    nwam_strerror(err));
		nwamd_event_do_not_send(event);
		return;
	}
	if ((object = nwamd_object_find(NWAM_OBJECT_TYPE_LOC,
	    event->event_object)) != NULL) {
		old_enabled = loc_is_enabled(object->nwamd_object_handle);
		nwam_loc_free(object->nwamd_object_handle);
		object->nwamd_object_handle = loch;
	} else {
		object = nwamd_object_init(NWAM_OBJECT_TYPE_LOC,
		    event->event_object, loch, NULL);
		object->nwamd_object_state = NWAM_STATE_OFFLINE;
		object->nwamd_object_aux_state =
		    NWAM_AUX_STATE_CONDITIONS_NOT_MET;
	}
	new_enabled = loc_is_enabled(loch);
	state = object->nwamd_object_state;
	nwamd_object_release(object);

	/*
	 * If this location is ONLINE and the value of the "enabled" property
	 * has not changed, then this location is getting refreshed because it
	 * was committed with changes.  Change states to re-activate itself.
	 * If the "enabled" property has changed, then this location is
	 * getting refreshed as part of a enable/disable action and there is
	 * no need to change states here.
	 */
	if (state == NWAM_STATE_ONLINE && old_enabled == new_enabled) {
		nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
		    event->event_object, NWAM_STATE_OFFLINE_TO_ONLINE,
		    NWAM_AUX_STATE_METHOD_RUNNING);
	}
}

/* Handle loc finish event */
void
nwamd_loc_handle_fini_event(nwamd_event_t event)
{
	nwamd_object_t object;

	nlog(LOG_DEBUG, "nwamd_loc_handle_fini_event(%s)",
	    event->event_object);

	/* Don't disable the location, as this can enable the Automatic loc */
	if ((object = nwamd_object_find(NWAM_OBJECT_TYPE_LOC,
	    event->event_object)) == NULL) {
		nlog(LOG_INFO, "nwamd_loc_handle_fini_event: "
		    "loc %s not found", event->event_object);
		nwamd_event_do_not_send(event);
		return;
	}
	nwamd_object_release_and_destroy(object);
}

void
nwamd_loc_handle_action_event(nwamd_event_t event)
{
	nwamd_object_t object;

	switch (event->event_msg->nwe_data.nwe_object_action.nwe_action) {
	case NWAM_ACTION_ENABLE:
		object = nwamd_object_find(NWAM_OBJECT_TYPE_LOC,
		    event->event_object);
		if (object == NULL) {
			nlog(LOG_ERR, "nwamd_loc_handle_action_event: "
			    "could not find location %s", event->event_object);
			nwamd_event_do_not_send(event);
			return;
		}
		if (object->nwamd_object_state == NWAM_STATE_ONLINE) {
			nlog(LOG_DEBUG, "nwamd_loc_handle_action_event: "
			    "location %s already online, nothing to do",
			    event->event_object);
			nwamd_object_release(object);
			return;
		}
		nwamd_object_release(object);

		nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
		    event->event_object, NWAM_STATE_OFFLINE_TO_ONLINE,
		    NWAM_AUX_STATE_METHOD_RUNNING);
		break;
	case NWAM_ACTION_DISABLE:
		object = nwamd_object_find(NWAM_OBJECT_TYPE_LOC,
		    event->event_object);
		if (object == NULL) {
			nlog(LOG_ERR, "nwamd_loc_handle_action_event: "
			    "could not find location %s", event->event_object);
			nwamd_event_do_not_send(event);
			return;
		}
		if (object->nwamd_object_state == NWAM_STATE_DISABLED) {
			nlog(LOG_DEBUG, "nwamd_loc_handle_action_event: "
			    "location %s already disabled, nothing to do",
			    event->event_object);
			nwamd_object_release(object);
			return;
		}
		nwamd_object_release(object);

		nwamd_object_set_state(NWAM_OBJECT_TYPE_LOC,
		    event->event_object, NWAM_STATE_ONLINE_TO_OFFLINE,
		    NWAM_AUX_STATE_MANUAL_DISABLE);
		break;
	case NWAM_ACTION_ADD:
	case NWAM_ACTION_REFRESH:
		nwamd_loc_handle_init_event(event);
		break;
	case NWAM_ACTION_DESTROY:
		nwamd_loc_handle_fini_event(event);
		break;
	default:
		nlog(LOG_INFO, "nwam_loc_handle_action_event: "
		    "unexpected action");
		break;
	}
}

void
nwamd_loc_handle_state_event(nwamd_event_t event)
{
	nwamd_object_t object;
	nwam_state_t new_state;
	nwam_aux_state_t new_aux_state;

	if ((object = nwamd_object_find(NWAM_OBJECT_TYPE_LOC,
	    event->event_object)) == NULL) {
		nlog(LOG_INFO, "nwamd_loc_handle_state_event: "
		    "state event for nonexistent loc %s", event->event_object);
		nwamd_event_do_not_send(event);
		return;
	}
	new_state = event->event_msg->nwe_data.nwe_object_state.nwe_state;
	new_aux_state =
	    event->event_msg->nwe_data.nwe_object_state.nwe_aux_state;

	if (new_state == object->nwamd_object_state &&
	    new_aux_state == object->nwamd_object_aux_state) {
		nlog(LOG_DEBUG, "nwamd_loc_handle_state_event: "
		    "loc %s already in state (%s , %s)",
		    object->nwamd_object_name,
		    nwam_state_to_string(new_state),
		    nwam_aux_state_to_string(new_aux_state));
		nwamd_object_release(object);
		return;
	}

	object->nwamd_object_state = new_state;
	object->nwamd_object_aux_state = new_aux_state;

	nlog(LOG_DEBUG, "nwamd_loc_handle_state_event: changing state for loc "
	    "%s to (%s , %s)", object->nwamd_object_name,
	    nwam_state_to_string(object->nwamd_object_state),
	    nwam_aux_state_to_string(object->nwamd_object_aux_state));

	nwamd_object_release(object);

	/*
	 * State machine for location.
	 */
	switch (new_state) {
	case NWAM_STATE_OFFLINE_TO_ONLINE:
		nwamd_loc_activate(event->event_object);
		break;
	case NWAM_STATE_ONLINE_TO_OFFLINE:
		/*
		 * Don't need to deactivate current location - condition check
		 * will activate another.  If the currently active location is
		 * being deactivated, then it is being manually deactivated;
		 * so also clear active_loc so condition checking is not
		 * confused.
		 */
		(void) pthread_mutex_lock(&active_loc_mutex);
		if (strcmp(event->event_object, active_loc) == 0)
			active_loc[0] = '\0';
		(void) pthread_mutex_unlock(&active_loc_mutex);
		nwamd_loc_check_conditions();
		break;
	case NWAM_STATE_DISABLED:
	case NWAM_STATE_OFFLINE:
	case NWAM_STATE_UNINITIALIZED:
	case NWAM_STATE_MAINTENANCE:
	case NWAM_STATE_DEGRADED:
	default:
		/* do nothing */
		break;
	}
}