/*
 * Asterisk -- An open source telephony toolkit.
 *
 * Copyright (C) 2026, Sangoma Technologies Corporation
 *
 * Joshua Colp <jcolp@sangoma.com>
 *
 * See https://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.
 */

/*** MODULEINFO
	<support_level>core</support_level>
 ***/

#include "asterisk.h"
#include "asterisk/_private.h"
#include "asterisk/module.h"
#include "asterisk/extension_state.h"
#include "asterisk/pbx.h"
#include "asterisk/stasis.h"
#include "asterisk/stasis_message_router.h"
#include "asterisk/astobj2.h"
#include "asterisk/cli.h"
#include "pbx_private.h"

/*** DOCUMENTATION
	<manager name="ExtensionStateList" language="en_US">
		<since>
			<version>13.0.0</version>
		</since>
		<synopsis>
			List the current known extension states.
		</synopsis>
		<syntax>
			<xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
		</syntax>
		<description>
			<para>This will list out all known extension states in a
			sequence of <replaceable>ExtensionStatus</replaceable> events.
			When finished, a <replaceable>ExtensionStateListComplete</replaceable> event
			will be emitted.</para>
		</description>
		<see-also>
			<ref type="manager">ExtensionState</ref>
			<ref type="function">HINT</ref>
			<ref type="function">EXTENSION_STATE</ref>
		</see-also>
		<responses>
			<list-elements>
				<xi:include xpointer="xpointer(/docs/managerEvent[@name='ExtensionStatus'])" />
			</list-elements>
			<managerEvent name="ExtensionStateListComplete" language="en_US">
				<managerEventInstance class="EVENT_FLAG_COMMAND">
					<since>
						<version>13.0.0</version>
					</since>
					<synopsis>
						Indicates the end of the list the current known extension states.
					</synopsis>
					<syntax>
						<parameter name="EventList">
							<para>Conveys the status of the event list.</para>
						</parameter>
						<parameter name="ListItems">
							<para>Conveys the number of statuses reported.</para>
						</parameter>
					</syntax>
				</managerEventInstance>
			</managerEvent>
		</responses>
	</manager>
 ***/

#define HINTDEVICE_DATA_LENGTH 16
AST_THREADSTORAGE(hintdevice_data);

/*! \brief Device state source feeding an extension state */
struct extension_state_device_source {
	/*! \brief The current state of the device - this is immutable */
	struct ast_extension_state_device_state_info *info;
	/*! \brief Synchronous subscription to the device state topic */
	struct stasis_subscription *device_state_subscription;
	/*! \brief The current version for this source */
	unsigned int version;
};

AST_VECTOR(device_state_sources_vector, struct extension_state_device_source *);

/*! \brief Extension state information */
struct extension_state {
	/*! \brief The current device snapshot for the extension */
	struct ast_extension_state_device_snapshot *device_snapshot;
	/*! \brief The current presence snapshot for the extension */
	struct ast_extension_state_presence_snapshot *presence_snapshot;
	/*! \brief The extension state topic for this extension */
	struct stasis_topic *extension_state_topic;
	/*! \brief Forwarder from per-extension topic to all topic */
	struct stasis_forward *extension_state_forwarder;
	/*! \brief Device state sources feeding the hint topic, and their forwarding */
	struct device_state_sources_vector device_state_sources;
	/*! \brief The string representation of all presence state sources feeding this extension state */
	char *presence_sources_string;
	/*! \brief The dialplan hint that last configured this extension state */
	struct ast_exten *hint_extension;
	/*! \brief The dialplan context */
	char dialplan_context[AST_MAX_CONTEXT];
	/*! \brief The dialplan extension */
	char dialplan_extension[AST_MAX_EXTENSION];
	/*! \brief The combined extension this state is for (extension@context) */
	char extension[0];
};

/*! \brief Number of buckets for extension states */
#ifdef LOW_MEMORY
#define EXTENSION_STATE_BUCKETS 17
#else
#define EXTENSION_STATE_BUCKETS 563
#endif

static const struct cfextension_states {
	int extension_state;
	const char * const text;
} extension_state_mappings[] = {
	{ AST_EXTENSION_NOT_INUSE,                     "Idle" },
	{ AST_EXTENSION_INUSE,                         "InUse" },
	{ AST_EXTENSION_BUSY,                          "Busy" },
	{ AST_EXTENSION_UNAVAILABLE,                   "Unavailable" },
	{ AST_EXTENSION_RINGING,                       "Ringing" },
	{ AST_EXTENSION_INUSE | AST_EXTENSION_RINGING, "InUse&Ringing" },
	{ AST_EXTENSION_ONHOLD,                        "Hold" },
	{ AST_EXTENSION_INUSE | AST_EXTENSION_ONHOLD,  "InUse&Hold" }
};

/*! \brief The global container of extension states */
static struct ao2_container *extension_states;

/*! \brief Topic which receives all extension state updates */
static struct stasis_topic *extension_state_topic_all;

/*! \brief Single presence state subscription, for all extension states */
static struct stasis_subscription *presence_state_sub;

/*! \brief Sort function for extension states */
AO2_STRING_FIELD_SORT_FN(extension_state, extension)

/*! \brief Compare function for extension states */
AO2_STRING_FIELD_CMP_FN(extension_state, extension)

/*! \brief Message type for extension state updates */
STASIS_MESSAGE_TYPE_DEFN(ast_extension_state_update_message_type);

/*!
 * \internal
 * \brief Destroy an extension state update message
 * \param obj The extension state update message to destroy
 */
static void extension_state_update_message_destroy(void *obj)
{
	struct ast_extension_state_update_message *update_message = obj;

	ao2_cleanup(update_message->old_device_snapshot);
	ao2_cleanup(update_message->new_device_snapshot);
	ao2_cleanup(update_message->old_presence_snapshot);
	ao2_cleanup(update_message->new_presence_snapshot);
}

/*!
 * \internal
 * \brief Create an extension state update message
 *
 * \param context The context of the extension
 * \param extension The extension
 * \param old_device_snapshot The old device state snapshot
 * \param new_device_snapshot The new device state snapshot
 * \param old_presence_snapshot The old presence state snapshot
 * \param new_presence_snapshot The new presence state snapshot
 * \retval An allocated extension state update message, or NULL on failure
 *
 * This function creates an extension state update message for the specified context, extension,
 * old device state snapshot, new device state snapshot, old presence state snapshot, and new presence state snapshot.
 */
static struct ast_extension_state_update_message *extension_state_update_message_create(const char *context,
	const char *extension, struct ast_extension_state_device_snapshot *old_device_snapshot,
	struct ast_extension_state_device_snapshot *new_device_snapshot, struct ast_extension_state_presence_snapshot *old_presence_snapshot,
	struct ast_extension_state_presence_snapshot *new_presence_snapshot)
{
	size_t context_len = strlen(context) + 1;
	size_t extension_len = strlen(extension) + 1;
	struct ast_extension_state_update_message *update_message;

	update_message = ao2_alloc_options(sizeof(*update_message) + context_len + extension_len,
		extension_state_update_message_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
	if (!update_message) {
		return NULL;
	}

	ast_copy_string(update_message->extension, extension, extension_len); /* Safe */
	update_message->context = update_message->extension + extension_len;
	ast_copy_string(update_message->context, context, context_len); /* Safe */

	update_message->old_device_snapshot = ao2_bump(old_device_snapshot);
	update_message->new_device_snapshot = ao2_bump(new_device_snapshot);
	update_message->old_presence_snapshot = ao2_bump(old_presence_snapshot);
	update_message->new_presence_snapshot = ao2_bump(new_presence_snapshot);

	return update_message;
}

/*!
 * \internal
 * \brief Destroy an extension state device source
 * \param source The extension state device source to destroy
 *
 * This function destroys an extension state device source by unsubscribing from the device state
 * topic and cleaning up the associated resources.
 */
static void extension_state_device_source_destroy(struct extension_state_device_source *source)
{
	if (source->device_state_subscription) {
		stasis_unsubscribe(source->device_state_subscription);
	}
	ao2_cleanup(source->info);
	ast_free(source);
}

/*!
 * \internal
 * \brief Allocate an extension device state info object
 *
 * \param device The device name
 * \param state The device state
 * \retval An allocated extension device state info object, or NULL on failure
 *
 * This function allocates an extension device state info object with the specified device name and state.
 */
static struct ast_extension_state_device_state_info *extension_state_device_state_info_alloc(const char *device,
	enum ast_device_state state)
{
	struct ast_extension_state_device_state_info *info;

	info = ao2_alloc_options(sizeof(*info) + strlen(device) + 1, NULL, AO2_ALLOC_OPT_LOCK_NOLOCK);
	if (!info) {
		return NULL;
	}

	info->state = state;
	strcpy(info->device, device); /* Safe */

	return info;
}

/*!
 * \internal
 * \brief Destroy an extension state device snapshot
 *
 * \param obj The extension state device snapshot to destroy
 *
 * This function destroys an extension state device snapshot by cleaning up
 * the causing device and additional devices.
 */
static void extension_state_device_snapshot_destroy(void *obj)
{
	struct ast_extension_state_device_snapshot *device_snapshot = obj;

	ao2_cleanup(device_snapshot->causing_device);
	AST_VECTOR_CALLBACK_VOID(&device_snapshot->additional_devices, ao2_cleanup);
	AST_VECTOR_FREE(&device_snapshot->additional_devices);
}

/*!
 * \internal
 * \brief Create an extension state device snapshot
 *
 * \param device_state The device state
 * \param device_state_sources The device state sources
 * \param causing_device The causing device
 * \retval An allocated extension state device snapshot, or NULL on failure
 *
 * This function creates an extension state device snapshot with the device state,
 * device state sources, and causing device.
 */
static struct ast_extension_state_device_snapshot *extension_state_device_snapshot_create(
	enum ast_extension_states device_state, struct device_state_sources_vector *device_state_sources,
	struct ast_extension_state_device_state_info *causing_device)
{
	struct ast_extension_state_device_snapshot *device_snapshot;
	int i;

	device_snapshot = ao2_alloc_options(sizeof(*device_snapshot),
		extension_state_device_snapshot_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
	if (!device_snapshot) {
		return NULL;
	}

	device_snapshot->state = device_state;
	device_snapshot->causing_device = ao2_bump(causing_device);
	if (AST_VECTOR_INIT(&device_snapshot->additional_devices, AST_VECTOR_SIZE(device_state_sources))) {
		ao2_ref(device_snapshot, -1);
		return NULL;
	}

	for (i = 0; i < AST_VECTOR_SIZE(device_state_sources); i++) {
		struct extension_state_device_source *source = AST_VECTOR_GET(device_state_sources, i);

		if (causing_device && source->info == causing_device) {
			continue;
		}

		AST_VECTOR_APPEND(&device_snapshot->additional_devices, ao2_bump(source->info));
	}

	return device_snapshot;
}

/*!
 * \internal
 * \brief Callback for device state changes
 *
 * \param userdata The extension state to update
 * \param sub The subscription
 * \param msg The device state message
 *
 * This function is called when a device state changes and updates the extension state
 * accordingly by aggregating the device states and publishing the new state.
 */
static void extension_state_device_state_cb(void *userdata, struct stasis_subscription *sub,
		struct stasis_message *msg)
{
	struct extension_state *state = userdata;
	struct ast_device_state_message *device_state;
	struct ast_devstate_aggregate agg;
	struct ast_extension_state_device_state_info *extension_device_state_info;
	int i;
	unsigned int updated = 0;
	enum ast_extension_states new_device_state;

	if (stasis_message_type(msg) != ast_device_state_message_type()) {
		return;
	}

	device_state = stasis_message_data(msg);

	/* We only care about the aggregate state */
	if (device_state->eid) {
		return;
	}

	ast_devstate_aggregate_init(&agg);

	/*
	 * Alrighty, the reason that we store an extension_device_state_info is to reduce the memory allocation that
	 * has to occur every time we get a device state update and have to construct a new message. If the extension
	 * state contains only a single device source we have to do this anyway, but if there's multiple then if we
	 * didn't store the result we'd be creating new ones every message to put in the additional_devices vector.
	 */
	extension_device_state_info = extension_state_device_state_info_alloc(device_state->device, device_state->state);
	if (!extension_device_state_info) {
		return;
	}

	ao2_lock(state);

	for (i = 0; i < AST_VECTOR_SIZE(&state->device_state_sources); i++) {
		struct extension_state_device_source *source = AST_VECTOR_GET(&state->device_state_sources, i);

		if (!strcmp(source->info->device, device_state->device)) {
			ao2_replace(source->info, extension_device_state_info);
			updated = 1;
		}

		ast_devstate_aggregate_add(&agg, source->info->state);
	}

	/* We don't really care about the device state info contents now, so we can drop the reference */
	ao2_ref(extension_device_state_info, -1);

	/*
	 * It's possible for a device state update to come in for a device which is no longer feeding this
	 * extension state if it has been updated, so only actually care about the new device state if a
	 * source has actually been updated.
	 */
	if (!updated) {
		ao2_unlock(state);
		return;
	}

	/*
	 * We actually update things and raise a message if the state is different, or if the state is ringing
	 * as that can actually just be an update that someone else is ringing the same extension.
	 */
	new_device_state = ast_devstate_to_extenstate(ast_devstate_aggregate_result(&agg));
	if ((state->device_snapshot->state != new_device_state) || (new_device_state & AST_EXTENSION_RINGING)) {
		struct ast_extension_state_device_snapshot *device_snapshot;
		struct ast_extension_state_update_message *update_message;
		struct stasis_message *message;

		/* Now above you probably noticed I dropped the reference for extension_device_state_info but now I'm
		 * passing it in here. Don't panic - a reference exists on the device state source still and since we
		 * have the state locked it can't go away.
		 */
		device_snapshot = extension_state_device_snapshot_create(new_device_state, &state->device_state_sources,
			extension_device_state_info);
		if (!device_snapshot) {
			ao2_unlock(state);
			return;
		}

		update_message = extension_state_update_message_create(state->dialplan_context, state->dialplan_extension,
			state->device_snapshot, device_snapshot, state->presence_snapshot, state->presence_snapshot);

		/* Even if we can't publish an update message we still ensure the local cached snapshot is up to date */
		ao2_replace(state->device_snapshot, device_snapshot);
		ao2_ref(device_snapshot, -1);

		if (!update_message) {
			ao2_unlock(state);
			return;
		}

		/* Inform any subscribers of the change to the device snapshot */
		message = stasis_message_create(ast_extension_state_update_message_type(), update_message);
		if (message) {
			stasis_publish(state->extension_state_topic, message);
			ao2_ref(message, -1);
		}

		ao2_ref(update_message, -1);
	}

	ao2_unlock(state);
}

/*!
 * \internal
 * \brief Allocate a device source for an extension state
 *
 * \param state The extension state to allocate the device source for
 * \param device The device to allocate the source for
 * \retval An allocated device source, or NULL on failure
 *
 * This function allocates a device source for an extension state by creating a device state source
 * and setting up the necessary subscriptions and references.
 */
static struct extension_state_device_source *extension_state_device_source_alloc(struct extension_state *state, const char *device)
{
	struct extension_state_device_source *source;
	struct stasis_topic *topic;

	/*
	 * Ensure that we have a direct device state topic for the device, note this is returned without a reference but
	 * is guaranteed to exist regardless.
	 */
	topic = ast_device_state_topic(device);
	if (!topic) {
		return NULL;
	}

	/*
	 * The device state source is only used within the extension state and is never
	 * passed around so the overhead of an ao2 object with reference counting is unnecessary.
	 */
	source = ast_calloc(1, sizeof(*source));
	if (!source) {
		return NULL;
	}

	source->info = extension_state_device_state_info_alloc(device, ast_device_state(device));
	if (!source->info) {
		extension_state_device_source_destroy(source);
		return NULL;
	}

	/*
	 * We do a synchronous subscription to the device state topic, as our callback is extremely
	 * short lived and the added overhead of queueing to a taskprocessor for another thread to handle
	 * it is just not worth it.
	 */
	source->device_state_subscription = stasis_subscribe_synchronous(topic, extension_state_device_state_cb, state);
	if (!source->device_state_subscription) {
		extension_state_device_source_destroy(source);
		return NULL;
	}

	stasis_subscription_accept_message_type(source->device_state_subscription, ast_device_state_message_type());
	stasis_subscription_set_filter(source->device_state_subscription, STASIS_SUBSCRIPTION_FILTER_SELECTIVE);

	return source;
}

/*!
 * \internal
 * \brief Destroy an extension state presence snapshot
 *
 * \param obj The extension state presence snapshot to destroy
 *
 * This function destroys an extension state presence snapshot by cleaning up
 * the presence snapshot.
 */
static void extension_state_presence_snapshot_destroy(void *obj)
{
	struct ast_extension_state_presence_snapshot *presence_snapshot = obj;

	ast_free(presence_snapshot->presence_subtype);
	ast_free(presence_snapshot->presence_message);
}

/*!
 * \internal
 * \brief Create an extension state presence snapshot
 *
 * \param presence_state The presence state
 * \param presence_subtype The presence subtype (can be NULL)
 * \param presence_message The presence message (can be NULL)
 * \retval An allocated extension state presence snapshot, or NULL on failure
 *
 * This function creates an extension state presence snapshot for the specified presence state,
 * presence subtype, and presence message.
 */
static struct ast_extension_state_presence_snapshot *extension_state_presence_snapshot_create(enum ast_presence_state presence_state,
	const char *presence_subtype, const char *presence_message)
{
	struct ast_extension_state_presence_snapshot *presence_snapshot;

	presence_snapshot = ao2_alloc_options(sizeof(*presence_snapshot), extension_state_presence_snapshot_destroy,
		AO2_ALLOC_OPT_LOCK_NOLOCK);
	if (!presence_snapshot) {
		return NULL;
	}

	/* To ensure that we don't give a partial snapshot we fail creation if any allocation fails */
	presence_snapshot->presence_state = presence_state;
	if (presence_subtype) {
		presence_snapshot->presence_subtype = ast_strdup(presence_subtype);
		if (!presence_snapshot->presence_subtype) {
			ao2_ref(presence_snapshot, -1);
			return NULL;
		}
	}
	if (presence_message) {
		presence_snapshot->presence_message = ast_strdup(presence_message);
		if (!presence_snapshot->presence_message) {
			ao2_ref(presence_snapshot, -1);
			return NULL;
		}
	}

	return presence_snapshot;
}

/*!
 * \brief device source non-matching version comparator for AST_VECTOR_REMOVE_CMP_UNORDERED()
 *
 * \param elem Element to compare against
 * \param value Value to compare with the vector element.
 *
 * \return 0 if element does not match.
 * \return Non-zero if element matches.
 */
#define DEVICE_SOURCE_ELEM_VERSION_CMP(elem, value) ((elem)->version != (value))

/*!
 * \internal
 * \brief Update the sources of an extension state
 *
 * \param state The extension state to update
 * \param exten The extension to update
 * \retval 0 on success, -1 on failure
 *
 * This function updates the sources of an extension state by parsing the app part
 * of the extension and updating the device and presence state sources.
 */
static int extension_state_update_sources(struct extension_state *state, struct ast_exten *exten)
{
	struct ast_str *str = ast_str_thread_get(&hintdevice_data, HINTDEVICE_DATA_LENGTH);
	char *devices, *device, *presence_state_sources;
	struct ast_devstate_aggregate agg;
	enum ast_extension_states new_device_state;
	unsigned int version;
	struct ast_extension_state_device_snapshot *device_snapshot = NULL;
	struct ast_extension_state_presence_snapshot *presence_snapshot = NULL;

	ast_str_set(&str, 0, "%s", ast_get_extension_app(exten));
	devices = ast_str_buffer(str);

	ao2_lock(state);

	/*
	 * The format of the app part of a hint is "[device[&device]],[presence[&presence]]" so
	 * we can just find the first occurrence of ',' in order to get to the presence sources.
	 */
	presence_state_sources = strchr(devices, ',');
	if (presence_state_sources) {
		*presence_state_sources++ = '\0';
	}

	ast_devstate_aggregate_init(&agg);

	version = ast_random();

	/* Devices are separated by '&' */
	while ((device = strsep(&devices, "&"))) {
		struct extension_state_device_source *source = NULL;
		int i;

		/* Skip any device names that are empty, as we can do nothing */
		if (ast_strlen_zero(device)) {
			continue;
		}

		for (i = 0; i < AST_VECTOR_SIZE(&state->device_state_sources); i++) {
			struct extension_state_device_source *existing_source = AST_VECTOR_GET(&state->device_state_sources, i);

			if (!strcmp(existing_source->info->device, device)) {
				source = existing_source;
				break;
			}
		}
		if (!source) {
			source = extension_state_device_source_alloc(state, device);
			if (!source) {
				ao2_unlock(state);
				return -1;
			}
			AST_VECTOR_APPEND(&state->device_state_sources, source);
		}

		ast_devstate_aggregate_add(&agg, source->info->state);
		source->version = version;
	}

	/* Do a pass and remove all old device sources */
	AST_VECTOR_REMOVE_ALL_CMP_UNORDERED(&state->device_state_sources, version,
		DEVICE_SOURCE_ELEM_VERSION_CMP, extension_state_device_source_destroy);

	/* If device state sources exist it is what produces the device state, otherwise we use the default */
	if (AST_VECTOR_SIZE(&state->device_state_sources)) {
		new_device_state = ast_devstate_to_extenstate(ast_devstate_aggregate_result(&agg));
	} else {
		new_device_state = AST_EXTENSION_UNAVAILABLE;
	}

	/* If the device state has changed, create a new snapshot */
	if (state->device_snapshot->state != new_device_state) {
		device_snapshot = extension_state_device_snapshot_create(new_device_state, &state->device_state_sources, NULL);
	}
	if (!device_snapshot) {
		/* If there's no new device snapshot we use the old one */
		device_snapshot = state->device_snapshot;
	}

	/* If the presence state has changed, create a new snapshot */
	if ((!state->presence_sources_string && presence_state_sources) ||
		(state->presence_sources_string && strcmp(state->presence_sources_string, presence_state_sources))) {
		enum ast_presence_state presence_state = AST_PRESENCE_NOT_SET;
		char *presence_subtype = NULL, *presence_message = NULL;

		ast_free(state->presence_sources_string);

		if (!ast_strlen_zero(presence_state_sources)) {
			state->presence_sources_string = ast_strdup(presence_state_sources);
			/* Presence state is also separated by & but only the presence state API can handle it and aggregate */
			presence_state = ast_presence_state(presence_state_sources, &presence_subtype,
				&presence_message);
		} else {
			state->presence_sources_string = NULL;
		}

		presence_snapshot = extension_state_presence_snapshot_create(presence_state, presence_subtype, presence_message);

		ast_free(presence_subtype);
		ast_free(presence_message);
	}
	if (!presence_snapshot) {
		/* If there's no new presence snapshot we use the old one */
		presence_snapshot = state->presence_snapshot;
	}

	/* If any snapshots have changed create an update message containing them */
	if (state->device_snapshot != device_snapshot || state->presence_snapshot != presence_snapshot) {
		struct ast_extension_state_update_message *update_message;

		update_message = extension_state_update_message_create(state->dialplan_context, state->dialplan_extension, state->device_snapshot,
			device_snapshot, state->presence_snapshot, presence_snapshot);
		if (update_message) {
			struct stasis_message *message = stasis_message_create(ast_extension_state_update_message_type(), update_message);

			if (message) {
				stasis_publish(state->extension_state_topic, message);
				ao2_ref(message, -1);
			}

			ao2_ref(update_message, -1);
		}

		/* If applicable, update the snapshots on the state to their new version */
		if (state->device_snapshot != device_snapshot) {
			ao2_replace(state->device_snapshot, device_snapshot);
			ao2_ref(device_snapshot, -1);
		}
		if (state->presence_snapshot != presence_snapshot) {
			ao2_replace(state->presence_snapshot, presence_snapshot);
			ao2_ref(presence_snapshot, -1);
		}
	}

	ao2_unlock(state);

	return 0;
}

/*!
 * \internal
 * \brief Destroy an extension state
 *
 * \param obj The extension state to destroy
 *
 * This function destroys an extension state by cleaning up its resources.
 */
static void extension_state_destroy(void *obj)
{
	struct extension_state *state = obj;

	ao2_cleanup(state->device_snapshot);
	ao2_cleanup(state->presence_snapshot);
	ao2_cleanup(state->extension_state_topic);

	ast_free(state->presence_sources_string);
}

/*! \brief Stasis message type for extension state remove messages */
STASIS_MESSAGE_TYPE_DEFN(ast_extension_state_remove_message_type);

/*!
 * \internal
 * \brief Create an extension state remove message
 *
 * \param context The context of the extension
 * \param extension The extension to remove
 * \retval A stasis message for the remove event, or NULL on failure
 *
 * This function creates an extension state remove message for the specified context and extension.
 */
static struct stasis_message *extension_state_remove_message_create(const char *context, const char *extension)
{
	size_t context_len = strlen(context) + 1;
	size_t extension_len = strlen(extension) + 1;
	struct ast_extension_state_remove_message *remove_message;
	struct stasis_message *message;

	remove_message = ao2_alloc_options(sizeof(*remove_message) + context_len + extension_len, NULL,
		AO2_ALLOC_OPT_LOCK_NOLOCK);
	if (!remove_message) {
		return NULL;
	}

	ast_copy_string(remove_message->extension, extension, extension_len); /* Safe */
	remove_message->context = remove_message->extension + extension_len;
	ast_copy_string(remove_message->context, context, context_len); /* Safe */

	message = stasis_message_create(ast_extension_state_remove_message_type(), remove_message);
	ao2_ref(remove_message, -1);

	return message;
}

/*!
 * \internal
 * \brief Shut down an extension state
 *
 * \param state The extension state to shut down
 *
 * This function shuts down an extension state by publishing a remove message to its topic
 * and cleaning up its resources.
 */
static void extension_state_shutdown(struct extension_state *state)
{
	struct stasis_message *remove_message;

	/*
	 * Shutting down an extension state requires us to publish to its topic so all subscribers
	 * know that it is going away. However, if the topic failed to be created then we have nothing
	 * to publish to and can just return.
	 */
	if (!state->extension_state_topic) {
		return;
	}

	/* Inform all subscribers that this extension state is being removed */
	remove_message = extension_state_remove_message_create(state->dialplan_context,
		state->dialplan_extension);
	if (remove_message) {
		stasis_publish(state->extension_state_topic, remove_message);
		ao2_ref(remove_message, -1);
	}

	AST_VECTOR_CALLBACK_VOID(&state->device_state_sources, extension_state_device_source_destroy);
	AST_VECTOR_FREE(&state->device_state_sources);

	stasis_forward_cancel(state->extension_state_forwarder);
}

/*!
 * \internal
 * \brief Callback for presence state messages
 *
 * \param unused Unused parameter
 * \param sub The stasis subscription
 * \param msg The stasis message
 *
 * This callback is invoked when a presence state message is received. It updates
 * the presence state of all extension states that are interested in the presence
 * state provider and publishes an extension state update message if it has changed.
 */
static void extension_state_presence_state_cb(void *unused, struct stasis_subscription *sub,
		struct stasis_message *msg)
{
	struct ast_presence_state_message *presence_state;
	struct ao2_iterator iter;
	struct extension_state *state;

	if (stasis_message_type(msg) != ast_presence_state_message_type()) {
		return;
	}

	presence_state = stasis_message_data(msg);

	ao2_lock(extension_states);
	iter = ao2_iterator_init(extension_states, 0);
	for (; (state = ao2_iterator_next(&iter)); ao2_ref(state, -1)) {
		enum ast_presence_state presence_state_new;
		char *presence_subtype, *presence_message;
		struct ast_extension_state_presence_snapshot *presence_snapshot;
		struct ast_extension_state_update_message *update_message;

		ao2_lock(state);

		/*
		 * We determine if this update is relevant to this extension state by seeing if the presence sources string
		 * even remotely contains the provider for this update. Worst case it's a substring and the calculated presence
		 * state is the same as before in which case we ignore it.
		 */
		if (!state->presence_sources_string || !strcasestr(state->presence_sources_string, presence_state->provider)) {
			ao2_unlock(state);
			continue;
		}

		/*
		 * Aggregation of presence state is done by requesting the current presence state with passing in a complete
		 * list of providers. This means that a presence state change message is just a notification to us to go and
		 * retrieve the new presence state. We don't just take it from the message itself. Since presence state is not
		 * as common as device state this is not a problem despite being inefficient in comparison to the device state
		 * implementation.
		 */
		presence_state_new = ast_presence_state(state->presence_sources_string, &presence_subtype, &presence_message);
		if (presence_state_new == AST_PRESENCE_INVALID) {
			/* For the invalid case we just ignore this update */
			ao2_unlock(state);
			continue;
		}

		if ((state->presence_snapshot->presence_state == presence_state_new) &&
			((!presence_subtype && !state->presence_snapshot->presence_subtype) ||
				(presence_subtype && state->presence_snapshot->presence_subtype &&
					!strcmp(presence_subtype, state->presence_snapshot->presence_subtype))) &&
			((!presence_message && !state->presence_snapshot->presence_message) ||
				(presence_message && state->presence_snapshot->presence_message &&
					!strcmp(presence_message, state->presence_snapshot->presence_message)))) {
			/* No change in presence state, so ignore this update */
			ao2_unlock(state);
			ast_free(presence_subtype);
			ast_free(presence_message);
			continue;
		}

		presence_snapshot = extension_state_presence_snapshot_create(presence_state_new, presence_subtype, presence_message);
		if (!presence_snapshot) {
			ao2_unlock(state);
			ast_free(presence_subtype);
			ast_free(presence_message);
			continue;
		}

		update_message = extension_state_update_message_create(state->dialplan_context,
			state->dialplan_extension, state->device_snapshot, state->device_snapshot, state->presence_snapshot, presence_snapshot);
		ao2_replace(state->presence_snapshot, presence_snapshot);
		ao2_ref(presence_snapshot, -1);
		if (update_message) {
			struct stasis_message *message = stasis_message_create(ast_extension_state_update_message_type(), update_message);

			if (message) {
				stasis_publish(state->extension_state_topic, message);
				ao2_ref(message, -1);
			}

			ao2_ref(update_message, -1);
		}

		ao2_unlock(state);

		ast_free(presence_subtype);
		ast_free(presence_message);
	}
	ao2_iterator_destroy(&iter);
	ao2_unlock(extension_states);
}

/*!
 * \internal
 *
 * \brief Allocate an extension state object
 * \param exten The extension
 * \param context The context
 * \retval A pointer to the allocated extension state, or NULL on failure
 *
 * This function allocates an extension state object with the specified extension and context.
 */
static struct extension_state *extension_state_alloc(struct ast_exten *exten, struct ast_context *context)
{
	char extension[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 2];
	struct extension_state *state;
	char *extension_state_topic_name;

	snprintf(extension, sizeof(extension), "%s@%s", ast_get_extension_name(exten), ast_get_context_name(context));

	/*
	 * Each individual extension state has its own lock to ensure that when updating it
	 * we do not cause problems for either the existing topic ingesting updates
	 * or any access to the extension state cached message.
	 */
	state = ao2_alloc(sizeof(*state) + strlen(extension) + 1, extension_state_destroy);
	if (!state) {
		return NULL;
	}

	ast_copy_string(state->dialplan_context, ast_get_context_name(context), sizeof(state->dialplan_context));
	ast_copy_string(state->dialplan_extension, ast_get_extension_name(exten), sizeof(state->dialplan_extension));
	strcpy(state->extension, extension); /* Safe */
	AST_VECTOR_INIT(&state->device_state_sources, 0);

	/* These are the default if no sources are present */
	state->device_snapshot = extension_state_device_snapshot_create(AST_EXTENSION_UNAVAILABLE,
		&state->device_state_sources, NULL);
	state->presence_snapshot = extension_state_presence_snapshot_create(AST_PRESENCE_NOT_SET, NULL, NULL);
	if (!state->device_snapshot || !state->presence_snapshot) {
		ao2_ref(state, -1);
		return NULL;
	}

	/*
	 * We don't actually access the contents of exten past guarantee of it being valid so we can safely
	 * store a pointer to just do pointer comparison.
	 */
	state->hint_extension = exten;

	/* Pattern match extensions don't have sources or a topic, so return early */
	if (extension[0] == '_') {
		return state;
	}

	/* We most likely have at least one device state source */
	if (AST_VECTOR_INIT(&state->device_state_sources, 1)) {
		ao2_ref(state, -1);
		return NULL;
	}

	if (ast_asprintf(&extension_state_topic_name, "extension_state:extension/%s", extension) < 0) {
		ao2_ref(state, -1);
		return NULL;
	}

	state->extension_state_topic = stasis_topic_create(extension_state_topic_name);
	ast_free(extension_state_topic_name);
	if (!state->extension_state_topic) {
		ao2_ref(state, -1);
		return NULL;
	}

	state->extension_state_forwarder = stasis_forward_all(state->extension_state_topic, extension_state_topic_all);
	if (!state->extension_state_forwarder) {
		ao2_ref(state, -1);
		return NULL;
	}

	return state;
}

/*!
 * \internal
 *
 * \brief Get an extension state object
 * \param chan The channel
 * \param context The context
 * \param extension The extension
 * \retval A pointer to the extension state, or NULL on failure
 *
 * This function gets an extension state object for the specified channel, context, and extension.
 * If the extension state does not exist due to being from a pattern match, it will be created.
 */
static struct extension_state *extension_state_get(struct ast_channel *chan, const char *context, const char *extension)
{
	struct extension_state *state;
	struct ast_exten *hint_exten;
	struct pbx_find_info q = { .stacklen = 0 };
	char location[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 2];

	/* We optimistically search for the extension state using the provided context and extension */
	snprintf(location, sizeof(location), "%s@%s", extension, context);
	state = ao2_find(extension_states, location, OBJ_SEARCH_KEY);
	if (state) {
		return state;
	}

	/*
	 * Pattern match extensions do exist within extension state for the purposes of listing them out,
	 * but they can't resolve down to anything else
	 */
	if (extension[0] == '_') {
		return NULL;
	}

	ast_wrlock_contexts();

	/*
	 * We can't use the provided context and extension as-is because an include
	 * could have resulted in the context being different than what was provided.
	 * To handle this we query the dialplan to find where the hint actually is.
	 * We also need to do this to determine if this is a pattern match or an explicit
	 * extension.
	 */
	hint_exten = pbx_find_extension(chan, NULL, &q, context, extension,
		PRIORITY_HINT, NULL, "", E_MATCH);
	if (!hint_exten) {
		/*
		 * The extension must ALWAYS exist in the dialplan in some capacity. It is
		 * either in the dialplan as an explicit extension or a pattern match.
		 */
		ast_unlock_contexts();
		return NULL;
	}

	if (ast_get_extension_name(hint_exten)[0] == '_') {
		/*
		 * If this resolved down to a pattern match that means this is the first request
		 * for this explicit extension so we need to add it to the dialplan which will create
		 * an extension state for it. It's possible for us to conflict with another thread but
		 * in that case the ast_add_extension call will fail and be a no-op and we will return
		 * the extension state the other thread created.
		 */
		ast_add_extension(q.foundcontext, 0, extension, PRIORITY_HINT, ast_get_extension_label(hint_exten),
			ast_get_extension_matchcid(hint_exten) ? ast_get_extension_cidmatch(hint_exten) : NULL,
			ast_get_extension_app(hint_exten), ast_strdup(ast_get_extension_app_data(hint_exten)), ast_free_ptr,
			ast_get_extension_registrar(hint_exten));
	}

	/* The extension state should already exist at this point */
	snprintf(location, sizeof(location), "%s@%s", extension, q.foundcontext);
	ast_unlock_contexts();

	return ao2_find(extension_states, location, OBJ_SEARCH_KEY);
}

struct ast_channel *ast_extension_state_get_device_causing_channel(const char *device, enum ast_device_state device_state)
{
	enum ast_channel_state search_state = 0; /* prevent false uninit warning */
	char match[AST_CHANNEL_NAME];
	struct ast_channel_iterator *chan_iter;
	struct ast_channel *chan, *channel = NULL;
	struct timeval chantime = {0, }; /* prevent false uninit warning */

	switch (device_state) {
	case AST_DEVICE_RINGING:
	case AST_DEVICE_RINGINUSE:
		/* find ringing channel */
		search_state = AST_STATE_RINGING;
		break;
	case AST_DEVICE_BUSY:
		/* find busy channel */
		search_state = AST_STATE_BUSY;
		break;
	case AST_DEVICE_ONHOLD:
	case AST_DEVICE_INUSE:
		/* find up channel */
		search_state = AST_STATE_UP;
		break;
	case AST_DEVICE_UNKNOWN:
	case AST_DEVICE_NOT_INUSE:
	case AST_DEVICE_INVALID:
	case AST_DEVICE_UNAVAILABLE:
	case AST_DEVICE_TOTAL /* not a state */:
		/* no channels are of interest */
		return NULL;
	}

	/* iterate over all channels of the device */
	snprintf(match, sizeof(match), "%s-", device);
	chan_iter = ast_channel_iterator_by_name_new(match, strlen(match));
	for (; (chan = ast_channel_iterator_next(chan_iter)); ast_channel_unref(chan)) {
		ast_channel_lock(chan);
		/* this channel's state doesn't match */
		if (search_state != ast_channel_state(chan)) {
			ast_channel_unlock(chan);
			continue;
		}
		/* any non-ringing channel will fit */
		if (search_state != AST_STATE_RINGING) {
			ast_channel_unlock(chan);
			channel = chan;
			break;
		}
		/* but we need the oldest ringing channel of the device to match with undirected pickup */
		if (!channel) {
			chantime = ast_channel_creationtime(chan);
			ast_channel_ref(chan); /* must ref it! */
			channel = chan;
		} else if (ast_tvcmp(ast_channel_creationtime(chan), chantime) < 0) {
			chantime = ast_channel_creationtime(chan);
			ast_channel_unref(channel);
			ast_channel_ref(chan); /* must ref it! */
			channel = chan;
		}
		ast_channel_unlock(chan);
	}
	ast_channel_iterator_destroy(chan_iter);

	return channel;
}

enum ast_extension_states ast_devstate_to_extenstate(enum ast_device_state devstate)
{
	switch (devstate) {
	case AST_DEVICE_ONHOLD:
		return AST_EXTENSION_ONHOLD;
	case AST_DEVICE_BUSY:
		return AST_EXTENSION_BUSY;
	case AST_DEVICE_UNKNOWN:
		return AST_EXTENSION_NOT_INUSE;
	case AST_DEVICE_UNAVAILABLE:
	case AST_DEVICE_INVALID:
		return AST_EXTENSION_UNAVAILABLE;
	case AST_DEVICE_RINGINUSE:
		return (AST_EXTENSION_INUSE | AST_EXTENSION_RINGING);
	case AST_DEVICE_RINGING:
		return AST_EXTENSION_RINGING;
	case AST_DEVICE_INUSE:
		return AST_EXTENSION_INUSE;
	case AST_DEVICE_NOT_INUSE:
		return AST_EXTENSION_NOT_INUSE;
	case AST_DEVICE_TOTAL: /* not a device state, included for completeness */
		break;
	}

	return AST_EXTENSION_NOT_INUSE;
}

const char *ast_extension_state2str(int extension_state)
{
	int i;

	for (i = 0; (i < ARRAY_LEN(extension_state_mappings)); i++) {
		if (extension_state_mappings[i].extension_state == extension_state)
			return extension_state_mappings[i].text;
	}
	return "Unknown";
}

/*!
 * \internal
 * \brief Handle the ExtensionStateList Manager action
 *
 * \param s The manager session
 * \param m The manager message
 * \retval 0 on success, -1 on failure
 *
 * This function handles the ExtensionStateList Manager action by returning a list of all extension states.
 */
static int action_extensionstatelist(struct mansession *s, const struct message *m)
{
	const char *action_id = astman_get_header(m, "ActionID");

	if (!ao2_container_count(extension_states)) {
		astman_send_error(s, m, "No dialplan hints are available");
		return 0;
	}

	astman_send_listack(s, m, "Extension Statuses will follow", "start");

	ao2_lock(extension_states);
	if (ao2_container_count(extension_states)) {
		struct ao2_iterator it_states;
		struct extension_state *state;
		int count = 0;

		it_states = ao2_iterator_init(extension_states, 0);
		for (; (state = ao2_iterator_next(&it_states)); ao2_ref(state, -1)) {
			if (state->extension[0] == '_') {
				continue;
			}

			++count;

			astman_append(s, "Event: ExtensionStatus\r\n");
			if (!ast_strlen_zero(action_id)) {
				astman_append(s, "ActionID: %s\r\n", action_id);
			}
			ao2_lock(state);
			astman_append(s,
			   "Exten: %s\r\n"
			   "Context: %s\r\n"
			   "Hint: %s\r\n"
			   "Status: %d\r\n"
			   "StatusText: %s\r\n\r\n",
			   state->dialplan_extension,
			   state->dialplan_context,
			   state->hint_extension ? ast_get_extension_app(state->hint_extension) : "None",
			   state->device_snapshot->state,
			   ast_extension_state2str(state->device_snapshot->state));
			ao2_unlock(state);
		}
		ao2_iterator_destroy(&it_states);
		astman_send_list_complete_start(s, m, "ExtensionStateListComplete", count);
		astman_send_list_complete_end(s);
	} else {
		astman_send_error(s, m, "No dialplan hints are available");
	}

	ao2_unlock(extension_states);

	return 0;
}

void pbx_extension_state_hint_set(struct ast_exten *exten, struct ast_context *context)
{
	char extension[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 2];
	struct extension_state *state;

	snprintf(extension, sizeof(extension), "%s@%s", ast_get_extension_name(exten), ast_get_context_name(context));

	ao2_lock(extension_states);

	state = ao2_find(extension_states, extension, OBJ_SEARCH_KEY | OBJ_NOLOCK);
	if (!state) {
		state = extension_state_alloc(exten, context);
		if (!state) {
			ast_log(LOG_WARNING, "Could not create extension state for hint '%s', it will be unavailable\n",
				extension);
			ao2_unlock(extension_states);
			return;
		}
		ao2_link_flags(extension_states, state, OBJ_NOLOCK);
	}

	state->hint_extension = exten;
	if (extension[0] != '_') {
		extension_state_update_sources(state, exten);
	}
	ao2_ref(state, -1);

	ao2_unlock(extension_states);
}

void pbx_extension_state_hint_remove(struct ast_exten *exten, struct ast_context *context)
{
	char extension[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 2];
	struct extension_state *state;

	snprintf(extension, sizeof(extension), "%s@%s", ast_get_extension_name(exten), ast_get_context_name(context));

	ao2_lock(extension_states);

	state = ao2_find(extension_states, extension, OBJ_SEARCH_KEY | OBJ_NOLOCK);

	/* If this is not the latest hint extension that configured this extension state it can't remove it */
	if (!state || state->hint_extension != exten) {
		ao2_unlock(extension_states);
		ao2_cleanup(state);
		return;
	}

	extension_state_shutdown(state);
	ao2_unlink_flags(extension_states, state, OBJ_NOLOCK);
	ao2_ref(state, -1);

	ao2_unlock(extension_states);
}

struct stasis_topic *ast_extension_state_topic_all(void)
{
	return extension_state_topic_all;
}

struct stasis_topic *ast_extension_state_topic(const char *exten, const char *context)
{
	struct extension_state *state;
	struct stasis_topic *topic;

	state = extension_state_get(NULL, context, exten);
	if (!state) {
		return NULL;
	}

	ao2_lock(state);
	topic = ao2_bump(state->extension_state_topic);
	ao2_unlock(state);
	ao2_ref(state, -1);

	return topic;
}

struct ast_extension_state_device_snapshot *ast_extension_state_get_latest_device_snapshot(struct ast_channel *chan,
	const char *exten, const char *context)
{
	struct extension_state *state;
	struct ast_extension_state_device_snapshot *device_snapshot;

	state = extension_state_get(chan, context, exten);
	if (!state) {
		return NULL;
	}

	ao2_lock(state);
	device_snapshot = ao2_bump(state->device_snapshot);
	ao2_unlock(state);
	ao2_ref(state, -1);

	return device_snapshot;
}

struct ast_extension_state_presence_snapshot *ast_extension_state_get_latest_presence_snapshot(struct ast_channel *chan,
	const char *exten, const char *context)
{
	struct extension_state *state;
	struct ast_extension_state_presence_snapshot *presence_snapshot;

	state = extension_state_get(chan, context, exten);
	if (!state) {
		return NULL;
	}

	ao2_lock(state);
	presence_snapshot = ao2_bump(state->presence_snapshot);
	ao2_unlock(state);
	ao2_ref(state, -1);

	return presence_snapshot;
}

/*!
 * \internal
 * \brief CLI command to show hints
 *
 * \param e The CLI entry
 * \param cmd The command
 * \param a The CLI arguments
 *
 * This function shows all registered hints in the CLI.
 */
static char *handle_show_hints(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
	struct extension_state *state;
	int num = 0;
	struct ao2_iterator i;

	switch (cmd) {
	case CLI_INIT:
		e->command = "core show hints";
		e->usage =
			"Usage: core show hints\n"
			"       List registered hints.\n"
			"       Hint details are shown in five columns. In order from left to right, they are:\n"
			"       1. Hint extension URI.\n"
			"       2. List of mapped device or presence state identifiers.\n"
			"       3. Current extension state. The aggregate of mapped device states.\n"
			"       4. Current presence state for the mapped presence state provider.\n"
			"       5. Watchers - number of subscriptions and other entities watching this hint.\n";
		return NULL;
	case CLI_GENERATE:
		return NULL;
	}

	if (ao2_container_count(extension_states) == 0) {
		ast_cli(a->fd, "There are no registered dialplan hints\n");
		return CLI_SUCCESS;
	}
	/* ... we have hints ... */
	ast_cli(a->fd, "\n    -= Registered Asterisk Dial Plan Hints =-\n");

	i = ao2_iterator_init(extension_states, 0);
	for (; (state = ao2_iterator_next(&i)); ao2_ref(state, -1)) {
		ao2_lock(state);
		ast_cli(a->fd, "%-30.30s: %-60.60s  State:%-15.15s Presence:%-15.15s Watchers %2zd\n",
			state->extension,
			state->hint_extension ? ast_get_extension_app(state->hint_extension) : "None",
			ast_extension_state2str(state->device_snapshot->state),
			ast_presence_state2str(state->presence_snapshot->presence_state),
			state->extension_state_topic ? stasis_topic_subscribers(state->extension_state_topic) : 0);
		ao2_unlock(state);

		num++;
	}
	ao2_iterator_destroy(&i);

	ast_cli(a->fd, "----------------\n");
	ast_cli(a->fd, "- %d hints registered\n", num);
	return CLI_SUCCESS;
}

/*!
 * \internal
 * \brief Complete the core show hint CLI command
 *
 * \param line The command line
 * \param word The word being completed
 * \param pos The position in the command
 * \param state The completion state
 *
 * This function completes the core show hint CLI command.
 */
static char *complete_core_show_hint(const char *line, const char *word, int pos, int state)
{
	struct extension_state *extstate;
	char *ret = NULL;
	int which = 0;
	int wordlen;
	struct ao2_iterator i;

	if (pos != 3)
		return NULL;

	wordlen = strlen(word);

	/* walk through all hints */
	i = ao2_iterator_init(extension_states, 0);
	for (; (extstate = ao2_iterator_next(&i)); ao2_ref(extstate, -1)) {
		ao2_lock(extstate);
		if (!strncasecmp(word, extstate->dialplan_extension, wordlen) && ++which > state) {
			ret = ast_strdup(extstate->dialplan_extension);
			ao2_unlock(extstate);
			ao2_ref(extstate, -1);
			break;
		}
		ao2_unlock(extstate);
	}
	ao2_iterator_destroy(&i);

	return ret;
}

/*!
 * \internal
 * \brief CLI support for listing registered dial plan hint
 *
 * \param e The CLI entry
 * \param cmd The command
 * \param a The CLI arguments
 *
 * This function handles the core show hint CLI command.
 */
static char *handle_show_hint(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
	struct extension_state *extstate;
	int num = 0, extenlen;
	struct ao2_iterator i;

	switch (cmd) {
	case CLI_INIT:
		e->command = "core show hint";
		e->usage =
			"Usage: core show hint <exten>\n"
			"       List registered hint.\n"
			"       Hint details are shown in five columns. In order from left to right, they are:\n"
			"       1. Hint extension URI.\n"
			"       2. List of mapped device or presence state identifiers.\n"
			"       3. Current extension state. The aggregate of mapped device states.\n"
			"       4. Current presence state for the mapped presence state provider.\n"
			"       5. Watchers - number of subscriptions and other entities watching this hint.\n";
		return NULL;
	case CLI_GENERATE:
		return complete_core_show_hint(a->line, a->word, a->pos, a->n);
	}

	if (a->argc < 4)
		return CLI_SHOWUSAGE;

	if (ao2_container_count(extension_states) == 0) {
		ast_cli(a->fd, "There are no registered dialplan hints\n");
		return CLI_SUCCESS;
	}

	extenlen = strlen(a->argv[3]);
	i = ao2_iterator_init(extension_states, 0);
	for (; (extstate = ao2_iterator_next(&i)); ao2_ref(extstate, -1)) {
		ao2_lock(extstate);
		if (!strncasecmp(extstate->extension, a->argv[3], extenlen)) {
			ast_cli(a->fd, "%-30.30s: %-60.60s  State:%-15.15s Presence:%-15.15s Watchers %2zd\n",
				extstate->extension,
				extstate->hint_extension ? ast_get_extension_app(extstate->hint_extension) : "None",
				ast_extension_state2str(extstate->device_snapshot->state),
				ast_presence_state2str(extstate->presence_snapshot->presence_state),
				extstate->extension_state_topic ? stasis_topic_subscribers(extstate->extension_state_topic) : 0);
			num++;
		}
		ao2_unlock(extstate);
	}
	ao2_iterator_destroy(&i);
	if (!num)
		ast_cli(a->fd, "No hints matching extension %s\n", a->argv[3]);
	else
		ast_cli(a->fd, "%d hint%s matching extension %s\n", num, (num!=1 ? "s":""), a->argv[3]);
	return CLI_SUCCESS;
}

static struct ast_cli_entry extension_state_cli[] = {
	AST_CLI_DEFINE(handle_show_hints, "Show dialplan hints"),
	AST_CLI_DEFINE(handle_show_hint, "Show dialplan hint"),
};

/*!
 * \internal
 * \brief Callback function to clean up an individual extension state.
 *
 * \param obj The extension state object
 * \param arg Additional argument (not used)
 * \param flags Flags for the callback
 * \return CMP_MATCH if the object was processed, 0 otherwise
 */
static int extension_state_cleanup_individual(void *obj, void *arg, int flags)
{
	struct extension_state *state = obj;

	extension_state_shutdown(state);

	return CMP_MATCH;
}

/*!
 * \internal
 * \brief Clean up the extension state subsystem, called at shutdown.
 */
static void extension_state_cleanup(void)
{
	ast_cli_unregister_multiple(extension_state_cli, ARRAY_LEN(extension_state_cli));
	ast_manager_unregister("ExtensionStateList");
	if (extension_states) {
		ao2_callback(extension_states, OBJ_NODATA | OBJ_MULTIPLE | OBJ_UNLINK, extension_state_cleanup_individual, NULL);
		ao2_ref(extension_states, -1);
	}
	presence_state_sub = stasis_unsubscribe_and_join(presence_state_sub);
	STASIS_MESSAGE_TYPE_CLEANUP(ast_extension_state_update_message_type);
	STASIS_MESSAGE_TYPE_CLEANUP(ast_extension_state_remove_message_type);
}

int ast_extension_state_init(void)
{
	if (STASIS_MESSAGE_TYPE_INIT(ast_extension_state_update_message_type) != 0) {
		return -1;
	}

	if (STASIS_MESSAGE_TYPE_INIT(ast_extension_state_remove_message_type) != 0) {
		return -1;
	}

	/* Initialize extension state subsystem */
	extension_states = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_MUTEX, 0,
		extension_state_sort_fn, extension_state_cmp_fn);
	if (!extension_states) {
		ast_log(LOG_ERROR, "Failed to allocate new states container\n");
		return -1;
	}

	extension_state_topic_all = stasis_topic_create("extension_state:all");
	if (!extension_state_topic_all) {
		ast_log(LOG_ERROR, "Failed to create extension state topic\n");
		return -1;
	}

	presence_state_sub = stasis_subscribe(ast_presence_state_topic_all(), extension_state_presence_state_cb, NULL);
	if (!presence_state_sub) {
		ast_log(LOG_ERROR, "Failed to create subscription to receive presence state updates\n");
		return -1;
	}
	stasis_subscription_accept_message_type(presence_state_sub, ast_presence_state_message_type());
	stasis_subscription_set_filter(presence_state_sub, STASIS_SUBSCRIPTION_FILTER_SELECTIVE);

	ast_cli_register_multiple(extension_state_cli, ARRAY_LEN(extension_state_cli));
	ast_manager_register_xml_core("ExtensionStateList", EVENT_FLAG_CALL | EVENT_FLAG_REPORTING, action_extensionstatelist);
	ast_register_cleanup(extension_state_cleanup);

	return 0;
}
