/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * FreeRDP Proxy Server
 *
 * Copyright 2019 Mati Shabtay <matishabtay@gmail.com>
 * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com>
 * Copyright 2019 Idan Freiberg <speidy@gmail.com>
 * Copyright 2021 Armin Novak <anovak@thincast.com>
 * Copyright 2021 Thincast Technologies GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <winpr/assert.h>
#include <winpr/cast.h>

#include <freerdp/config.h>

#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/client/cmdline.h>

#include <freerdp/server/proxy/proxy_log.h>
#include <freerdp/channels/drdynvc.h>
#include <freerdp/channels/encomsp.h>
#include <freerdp/channels/rdpdr.h>
#include <freerdp/channels/rdpsnd.h>
#include <freerdp/channels/cliprdr.h>
#include <freerdp/channels/channels.h>

#include "pf_client.h"
#include "pf_channel.h"
#include <freerdp/server/proxy/proxy_context.h>
#include "pf_update.h"
#include "pf_input.h"
#include <freerdp/server/proxy/proxy_config.h>
#include "proxy_modules.h"
#include "pf_utils.h"
#include "channels/pf_channel_rdpdr.h"
#include "channels/pf_channel_smartcard.h"

#define TAG PROXY_TAG("client")

static void channel_data_free(void* obj);

WINPR_ATTR_NODISCARD
static BOOL proxy_server_reactivate(rdpContext* ps, const rdpContext* pc)
{
	WINPR_ASSERT(ps);
	WINPR_ASSERT(pc);

	if (!pf_context_copy_settings(ps->settings, pc->settings))
		return FALSE;

	/*
	 * DesktopResize causes internal function rdp_server_reactivate to be called,
	 * which causes the reactivation.
	 */
	WINPR_ASSERT(ps->update);
	return (ps->update->DesktopResize(ps));
}

static void pf_client_on_error_info(void* ctx, const ErrorInfoEventArgs* e)
{
	pClientContext* pc = (pClientContext*)ctx;
	pServerContext* ps = nullptr;

	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	WINPR_ASSERT(e);
	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);

	if (e->code == ERRINFO_NONE)
		return;

	PROXY_LOG_WARN(TAG, pc, "received ErrorInfo PDU. code=0x%08" PRIu32 ", message: %s", e->code,
	               freerdp_get_error_info_string(e->code));

	/* forward error back to client */
	freerdp_set_error_info(ps->context.rdp, e->code);
	if (!freerdp_send_error_info(ps->context.rdp))
	{
		PROXY_LOG_WARN(TAG, pc, "[fail] reply ErrorInfo PDU. code=0x%08" PRIu32 ", message: %s",
		               e->code, freerdp_get_error_info_string(e->code));
	}
}

static void pf_client_on_activated(WINPR_ATTR_UNUSED void* ctx,
                                   WINPR_ATTR_UNUSED const ActivatedEventArgs* e)
{
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_load_rdpsnd(pClientContext* pc)
{
	rdpContext* context = (rdpContext*)pc;

	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	/*
	 * if AudioOutput is enabled in proxy and client connected with rdpsnd, use proxy as rdpsnd
	 * backend. Otherwise, use sys:fake.
	 */
	if (!freerdp_static_channel_collection_find(context->settings, RDPSND_CHANNEL_NAME))
	{
		const char* params[2] = { RDPSND_CHANNEL_NAME, "sys:fake" };

		if (!freerdp_client_add_static_channel(context->settings, ARRAYSIZE(params), params))
			return FALSE;
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_use_peer_load_balance_info(pClientContext* pc)
{
	pServerContext* ps = nullptr;
	rdpSettings* settings = nullptr;
	DWORD lb_info_len = 0;
	const char* lb_info = nullptr;

	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);
	settings = pc->context.settings;
	WINPR_ASSERT(settings);

	lb_info = freerdp_nego_get_routing_token(&ps->context, &lb_info_len);
	if (!lb_info)
		return TRUE;

	return freerdp_settings_set_pointer_len(settings, FreeRDP_LoadBalanceInfo, lb_info,
	                                        lb_info_len);
}

WINPR_ATTR_NODISCARD
static BOOL str_is_empty(const char* str)
{
	if (!str)
		return TRUE;
	if (strlen(str) == 0)
		return TRUE;
	return FALSE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_use_proxy_smartcard_auth(const rdpSettings* settings)
{
	BOOL enable = freerdp_settings_get_bool(settings, FreeRDP_SmartcardLogon);
	const char* key = freerdp_settings_get_string(settings, FreeRDP_SmartcardPrivateKey);
	const char* cert = freerdp_settings_get_string(settings, FreeRDP_SmartcardCertificate);

	if (!enable)
		return FALSE;

	if (str_is_empty(key))
		return FALSE;

	if (str_is_empty(cert))
		return FALSE;

	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_pre_connect(freerdp* instance)
{
	pClientContext* pc = nullptr;
	pServerContext* ps = nullptr;
	const proxyConfig* config = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_ASSERT(instance);
	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);
	WINPR_ASSERT(ps->pdata);
	config = ps->pdata->config;
	WINPR_ASSERT(config);
	settings = instance->context->settings;
	WINPR_ASSERT(settings);

	/*
	 * as the client's settings are copied from the server's, GlyphSupportLevel might not be
	 * GLYPH_SUPPORT_NONE. the proxy currently do not support GDI & GLYPH_SUPPORT_CACHE, so
	 * GlyphCacheSupport must be explicitly set to GLYPH_SUPPORT_NONE.
	 *
	 * Also, OrderSupport need to be zeroed, because it is currently not supported.
	 */
	if (!freerdp_settings_set_uint32(settings, FreeRDP_GlyphSupportLevel, GLYPH_SUPPORT_NONE))
		return FALSE;

	void* OrderSupport = freerdp_settings_get_pointer_writable(settings, FreeRDP_OrderSupport);
	ZeroMemory(OrderSupport, 32);

	if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, DRDYNVC_SVC_CHANNEL_NAME))
	{
		if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE))
			return FALSE;
	}

	/* Multimon */
	if (!freerdp_settings_set_bool(settings, FreeRDP_UseMultimon, TRUE))
		return FALSE;

	/* Sound */
	if (!freerdp_settings_set_bool(settings, FreeRDP_AudioCapture, config->AudioInput) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, config->AudioOutput) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection,
	                               config->DeviceRedirection) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl,
	                               config->DisplayControl) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_MultiTouchInput, config->Multitouch))
		return FALSE;

	if (config->RemoteApp)
	{
		if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, RAIL_SVC_CHANNEL_NAME))
		{
			if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteApplicationMode, TRUE))
				return FALSE;
		}
	}

	if (config->DeviceRedirection)
	{
		if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, RDPDR_SVC_CHANNEL_NAME))
		{
			if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE))
				return FALSE;
		}
	}

	/* Display control */
	if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, config->DisplayControl))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate,
	                               config->DisplayControl))
		return FALSE;

	if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, ENCOMSP_SVC_CHANNEL_NAME))
	{
		if (!freerdp_settings_set_bool(settings, FreeRDP_EncomspVirtualChannel, TRUE))
			return FALSE;
	}

	if (config->Clipboard)
	{
		if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, CLIPRDR_SVC_CHANNEL_NAME))
		{
			if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, config->Clipboard))
				return FALSE;
		}
	}

	if (!freerdp_settings_set_bool(settings, FreeRDP_AutoReconnectionEnabled, TRUE))
		return FALSE;

	if (PubSub_SubscribeErrorInfo(instance->context->pubSub, pf_client_on_error_info) < 0)
		return FALSE;
	if (PubSub_SubscribeActivated(instance->context->pubSub, pf_client_on_activated) < 0)
		return FALSE;
	if (!pf_client_use_peer_load_balance_info(pc))
		return FALSE;

	return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_PRE_CONNECT, pc->pdata, pc);
}

/** @brief arguments for updateBackIdFn */
typedef struct
{
	pServerContext* ps;
	const char* name;
	UINT32 backId;
} UpdateBackIdArgs;

WINPR_ATTR_NODISCARD
static BOOL updateBackIdFn(WINPR_ATTR_UNUSED const void* key, void* value, void* arg)
{
	pServerStaticChannelContext* current = (pServerStaticChannelContext*)value;
	UpdateBackIdArgs* updateArgs = (UpdateBackIdArgs*)arg;

	if (strcmp(updateArgs->name, current->channel_name) != 0)
		return TRUE;

	current->back_channel_id = updateArgs->backId;
	if (!HashTable_Insert(updateArgs->ps->channelsByBackId, &current->back_channel_id, current))
	{
		WLog_ERR(TAG, "error inserting channel in channelsByBackId table");
	}
	return FALSE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_update_back_id(pServerContext* ps, const char* name, UINT32 backId)
{
	UpdateBackIdArgs res = { ps, name, backId };

	return HashTable_Foreach(ps->channelsByFrontId, updateBackIdFn, &res) == FALSE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_load_channels(freerdp* instance)
{
	pClientContext* pc = nullptr;
	pServerContext* ps = nullptr;
	const proxyConfig* config = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_ASSERT(instance);
	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);
	WINPR_ASSERT(ps->pdata);
	config = ps->pdata->config;
	WINPR_ASSERT(config);
	settings = instance->context->settings;
	WINPR_ASSERT(settings);
	/**
	 * Load all required plugins / channels / libraries specified by current
	 * settings.
	 */
	PROXY_LOG_INFO(TAG, pc, "Loading addins");

	if (!pf_client_load_rdpsnd(pc))
	{
		PROXY_LOG_ERR(TAG, pc, "Failed to load rdpsnd client");
		return FALSE;
	}

	if (!pf_utils_is_passthrough(config))
	{
		if (!freerdp_client_load_addins(instance->context->channels, settings))
		{
			PROXY_LOG_ERR(TAG, pc, "Failed to load addins");
			return FALSE;
		}
	}
	else
	{
		if (!pf_channel_rdpdr_client_new(pc))
			return FALSE;
#if defined(WITH_PROXY_EMULATE_SMARTCARD)
		if (!pf_channel_smartcard_client_new(pc))
			return FALSE;
#endif
		/* Copy the current channel settings from the peer connection to the client. */
		if (!freerdp_channels_from_mcs(settings, &ps->context))
			return FALSE;

		/* Filter out channels we do not want */
		{
			CHANNEL_DEF* channels = (CHANNEL_DEF*)freerdp_settings_get_pointer_array_writable(
			    settings, FreeRDP_ChannelDefArray, 0);
			UINT32 size = freerdp_settings_get_uint32(settings, FreeRDP_ChannelCount);
			UINT32 id = MCS_GLOBAL_CHANNEL_ID + 1;

			WINPR_ASSERT(channels || (size == 0));

			UINT32 x = 0;
			for (; x < size;)
			{
				CHANNEL_DEF* cur = &channels[x];
				proxyChannelDataEventInfo dev = WINPR_C_ARRAY_INIT;

				dev.channel_name = cur->name;
				dev.flags = cur->options;

				/* Filter out channels blocked by config */
				if (!pf_modules_run_filter(pc->pdata->module,
				                           FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE, pc->pdata,
				                           &dev))
				{
					const size_t s = size - MIN(size, x + 1);
					memmove(cur, &cur[1], sizeof(CHANNEL_DEF) * s);
					size--;
				}
				else
				{
					if (!pf_client_update_back_id(ps, cur->name, id++))
					{
						WLog_ERR(TAG, "unable to update backid for channel %s", cur->name);
						return FALSE;
					}
					x++;
				}
			}

			if (!freerdp_settings_set_uint32(settings, FreeRDP_ChannelCount, x))
				return FALSE;
		}
	}
	return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_LOAD_CHANNELS, pc->pdata, pc);
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_receive_channel_data_hook(freerdp* instance, UINT16 channelId,
                                                const BYTE* xdata, size_t xsize, UINT32 flags,
                                                size_t totalSize)
{
	pClientContext* pc = nullptr;
	pServerContext* ps = nullptr;
	proxyData* pdata = nullptr;
	pServerStaticChannelContext* channel = nullptr;
	UINT64 channelId64 = channelId;

	WINPR_ASSERT(instance);
	WINPR_ASSERT(xdata || (xsize == 0));

	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);

	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);

	pdata = ps->pdata;
	WINPR_ASSERT(pdata);

	channel = HashTable_GetItemValue(ps->channelsByBackId, &channelId64);
	if (!channel)
		return TRUE;

	WINPR_ASSERT(channel->onBackData);
	switch (channel->onBackData(pdata, channel, xdata, xsize, flags, totalSize))
	{
		case PF_CHANNEL_RESULT_PASS:
			/* Ignore messages for channels that can not be mapped.
			 * The client might not have enabled support for this specific channel,
			 * so just drop the message. */
			if (channel->front_channel_id == 0)
				return TRUE;

			return ps->context.peer->SendChannelPacket(
			    ps->context.peer, WINPR_ASSERTING_INT_CAST(UINT16, channel->front_channel_id),
			    totalSize, flags, xdata, xsize);
		case PF_CHANNEL_RESULT_DROP:
			return TRUE;
		case PF_CHANNEL_RESULT_ERROR:
		default:
			return FALSE;
	}
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_on_server_heartbeat(freerdp* instance, BYTE period, BYTE count1, BYTE count2)
{
	pClientContext* pc = nullptr;
	pServerContext* ps = nullptr;

	WINPR_ASSERT(instance);
	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	ps = pc->pdata->ps;
	WINPR_ASSERT(ps);

	return freerdp_heartbeat_send_heartbeat_pdu(ps->context.peer, period, count1, count2);
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_send_channel_data(pClientContext* pc, const proxyChannelDataEventInfo* ev)
{
	WINPR_ASSERT(pc);
	WINPR_ASSERT(ev);

	return Queue_Enqueue(pc->cached_server_channel_data, ev);
}

WINPR_ATTR_NODISCARD
static BOOL sendQueuedChannelData(pClientContext* pc)
{
	BOOL rc = TRUE;

	WINPR_ASSERT(pc);

	if (pc->connected)
	{
		proxyChannelDataEventInfo* ev = nullptr;

		Queue_Lock(pc->cached_server_channel_data);
		while (rc && (ev = Queue_Dequeue(pc->cached_server_channel_data)))
		{
			UINT16 channelId = 0;
			WINPR_ASSERT(pc->context.instance);

			channelId = freerdp_channels_get_id_by_name(pc->context.instance, ev->channel_name);
			/* Ignore unmappable channels */
			if ((channelId == 0) || (channelId == UINT16_MAX))
				rc = TRUE;
			else
			{
				WINPR_ASSERT(pc->context.instance->SendChannelPacket);
				rc = pc->context.instance->SendChannelPacket(pc->context.instance, channelId,
				                                             ev->total_size, ev->flags, ev->data,
				                                             ev->data_len);
			}
			channel_data_free(ev);
		}

		Queue_Unlock(pc->cached_server_channel_data);
	}

	return rc;
}

/**
 * Called after a RDP connection was successfully established.
 * Settings might have changed during negotiation of client / server feature
 * support.
 *
 * Set up local framebuffers and painting callbacks.
 * If required, register pointer callbacks to change the local mouse cursor
 * when hovering over the RDP window
 */
WINPR_ATTR_NODISCARD
static BOOL pf_client_post_connect(freerdp* instance)
{
	WINPR_ASSERT(instance);
	rdpContext* context = instance->context;
	WINPR_ASSERT(context);
	rdpUpdate* update = context->update;
	WINPR_ASSERT(update);
	pClientContext* pc = (pClientContext*)context;
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	rdpContext* ps = (rdpContext*)pc->pdata->ps;
	WINPR_ASSERT(ps);

	if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_POST_CONNECT, pc->pdata, pc))
		return FALSE;

	if (!gdi_init(instance, PIXEL_FORMAT_BGRA32))
		return FALSE;

	WINPR_ASSERT(freerdp_settings_get_bool(context->settings, FreeRDP_SoftwareGdi));

	pf_client_register_update_callbacks(update);

	/* virtual channels receive data hook */
	pc->client_receive_channel_data_original = instance->ReceiveChannelData;
	instance->ReceiveChannelData = pf_client_receive_channel_data_hook;

	instance->heartbeat->ServerHeartbeat = pf_client_on_server_heartbeat;

	pc->connected = TRUE;

	/* Send cached channel data */
	if (!sendQueuedChannelData(pc))
		return FALSE;

	/*
	 * after the connection fully established and settings were negotiated with target server,
	 * send a reactivation sequence to the client with the negotiated settings. This way,
	 * settings are synchorinized between proxy's peer and and remote target.
	 */
	return proxy_server_reactivate(ps, context);
}

/* This function is called whether a session ends by failure or success.
 * Clean up everything allocated by pre_connect and post_connect.
 */
static void pf_client_post_disconnect(freerdp* instance)
{
	pClientContext* pc = nullptr;
	proxyData* pdata = nullptr;

	if (!instance)
		return;

	if (!instance->context)
		return;

	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	pdata = pc->pdata;
	WINPR_ASSERT(pdata);

#if defined(WITH_PROXY_EMULATE_SMARTCARD)
	pf_channel_smartcard_client_free(pc);
#endif

	pf_channel_rdpdr_client_free(pc);

	pc->connected = FALSE;
	(void)pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_POST_DISCONNECT, pc->pdata, pc);

	PubSub_UnsubscribeErrorInfo(instance->context->pubSub, pf_client_on_error_info);
	gdi_free(instance);

	/* Only close the connection if NLA fallback process is done */
	if (!pc->allow_next_conn_failure)
		proxy_data_abort_connect(pdata);
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_redirect(freerdp* instance)
{
	if (!instance)
		return FALSE;

	if (!instance->context)
		return FALSE;

	pClientContext* pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);

#if defined(WITH_PROXY_EMULATE_SMARTCARD)
	pf_channel_smartcard_client_reset(pc);
#endif
	pf_channel_rdpdr_client_reset(pc);

	return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_REDIRECT, pc->pdata, pc);
}

/*
 * pf_client_should_retry_without_nla:
 *
 * returns TRUE if in case of connection failure, the client should try again without NLA.
 * Otherwise, returns FALSE.
 */
WINPR_ATTR_NODISCARD
static BOOL pf_client_should_retry_without_nla(pClientContext* pc)
{
	rdpSettings* settings = nullptr;
	const proxyConfig* config = nullptr;

	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	settings = pc->context.settings;
	WINPR_ASSERT(settings);
	config = pc->pdata->config;
	WINPR_ASSERT(config);

	if (!config->ClientAllowFallbackToTls ||
	    !freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity))
		return FALSE;

	return config->ClientTlsSecurity || config->ClientRdpSecurity;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_set_security_settings(pClientContext* pc)
{
	WINPR_ASSERT(pc);
	WINPR_ASSERT(pc->pdata);
	rdpSettings* settings = pc->context.settings;
	WINPR_ASSERT(settings);
	const proxyConfig* config = pc->pdata->config;
	WINPR_ASSERT(config);

	if (!freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, config->ClientRdpSecurity))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, config->ClientTlsSecurity))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, config->ClientNlaSecurity))
		return FALSE;

	if (pf_client_use_proxy_smartcard_auth(settings))
	{
		/* Smartcard authentication requires smartcard redirection to be enabled */
		if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSmartCards, TRUE))
			return FALSE;

		/* Reset username/domain, we will get that info later from the sc cert */
		if (!freerdp_settings_set_string(settings, FreeRDP_Username, nullptr))
			return FALSE;
		if (!freerdp_settings_set_string(settings, FreeRDP_Domain, nullptr))
			return FALSE;
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_connect_without_nla(pClientContext* pc)
{
	freerdp* instance = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_ASSERT(pc);
	instance = pc->context.instance;
	WINPR_ASSERT(instance);

	if (!freerdp_context_reset(instance))
		return FALSE;

	settings = pc->context.settings;
	WINPR_ASSERT(settings);

	/* If already disabled abort early. */
	if (!freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity))
		return FALSE;

	/* disable NLA */
	if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, FALSE))
		return FALSE;

	/* do not allow next connection failure */
	pc->allow_next_conn_failure = FALSE;
	return freerdp_connect(instance);
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_connect(freerdp* instance)
{
	pClientContext* pc = nullptr;
	rdpSettings* settings = nullptr;
	BOOL rc = FALSE;
	BOOL retry = FALSE;

	WINPR_ASSERT(instance);
	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);
	settings = instance->context->settings;
	WINPR_ASSERT(settings);

	if (!pf_client_set_security_settings(pc))
		return FALSE;

	if (pf_client_should_retry_without_nla(pc))
		retry = pc->allow_next_conn_failure = TRUE;

	PROXY_LOG_INFO(TAG, pc, "connecting using client info: Username: %s, Domain: %s",
	               freerdp_settings_get_string(settings, FreeRDP_Username),
	               freerdp_settings_get_string(settings, FreeRDP_Domain));
	PROXY_LOG_INFO(TAG, pc, "connecting using security settings: rdp=%d, tls=%d, nla=%d",
	               freerdp_settings_get_bool(settings, FreeRDP_RdpSecurity),
	               freerdp_settings_get_bool(settings, FreeRDP_TlsSecurity),
	               freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity));

	if (!freerdp_connect(instance))
	{
		if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_LOGIN_FAILURE, pc->pdata, pc))
			goto out;

		if (!retry)
			goto out;

		PROXY_LOG_ERR(TAG, pc, "failed to connect with NLA. retrying to connect without NLA");
		if (!pf_client_connect_without_nla(pc))
		{
			PROXY_LOG_ERR(TAG, pc, "pf_client_connect_without_nla failed!");
			goto out;
		}
	}

	rc = TRUE;
out:
	pc->allow_next_conn_failure = FALSE;
	return rc;
}

/**
 * RDP main loop.
 * Connects RDP, loops while running and handles event and dispatch, cleans up
 * after the connection ends.
 */
WINPR_ATTR_NODISCARD
static DWORD WINAPI pf_client_thread_proc(pClientContext* pc)
{
	freerdp* instance = nullptr;
	proxyData* pdata = nullptr;
	DWORD nCount = 0;
	DWORD status = 0;
	HANDLE handles[MAXIMUM_WAIT_OBJECTS] = WINPR_C_ARRAY_INIT;

	WINPR_ASSERT(pc);

	instance = pc->context.instance;
	WINPR_ASSERT(instance);

	pdata = pc->pdata;
	WINPR_ASSERT(pdata);
	/*
	 * during redirection, freerdp's abort event might be overridden (reset) by the library, after
	 * the server set it in order to shutdown the connection. it means that the server might signal
	 * the client to abort, but the library code will override the signal and the client will
	 * continue its work instead of exiting. That's why the client must wait on `pdata->abort_event`
	 * too, which will never be modified by the library.
	 */
	handles[nCount++] = pdata->abort_event;

	if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_CLIENT_INIT_CONNECT, pdata, pc))
	{
		proxy_data_abort_connect(pdata);
		goto end;
	}

	if (!pf_client_connect(instance))
	{
		proxy_data_abort_connect(pdata);
		goto end;
	}
	handles[nCount++] = Queue_Event(pc->cached_server_channel_data);

	while (!freerdp_shall_disconnect_context(instance->context))
	{
		UINT32 tmp = freerdp_get_event_handles(instance->context, &handles[nCount],
		                                       ARRAYSIZE(handles) - nCount);

		if (tmp == 0)
		{
			PROXY_LOG_ERR(TAG, pc, "freerdp_get_event_handles failed!");
			break;
		}

		status = WaitForMultipleObjects(nCount + tmp, handles, FALSE, INFINITE);

		if (status == WAIT_FAILED)
		{
			WLog_ERR(TAG, "WaitForMultipleObjects failed with %" PRIu32 "", status);
			break;
		}

		/* abort_event triggered */
		if (status == WAIT_OBJECT_0)
			break;

		if (freerdp_shall_disconnect_context(instance->context))
			break;

		if (proxy_data_shall_disconnect(pdata))
			break;

		if (!freerdp_check_event_handles(instance->context))
		{
			if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_SUCCESS)
				WLog_ERR(TAG, "Failed to check FreeRDP event handles");

			break;
		}
		if (!sendQueuedChannelData(pc))
			break;
	}

	freerdp_disconnect(instance);

end:
	(void)pf_modules_run_hook(pdata->module, HOOK_TYPE_CLIENT_UNINIT_CONNECT, pdata, pc);

	return 0;
}

WINPR_ATTR_NODISCARD
static int pf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type)
{
	const char* str_data = freerdp_get_logon_error_info_data(data);
	const char* str_type = freerdp_get_logon_error_info_type(type);

	if (!instance || !instance->context)
		return -1;

	WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type);
	return 1;
}

static void pf_client_context_free(freerdp* instance, rdpContext* context)
{
	pClientContext* pc = (pClientContext*)context;
	WINPR_UNUSED(instance);

	if (!pc)
		return;

	pc->sendChannelData = nullptr;
	Queue_Free(pc->cached_server_channel_data);
	Stream_Free(pc->remote_pem, TRUE);
	free(pc->remote_hostname);
	free(pc->computerName.v);
	HashTable_Free(pc->interceptContextMap);
}

WINPR_ATTR_NODISCARD
static int pf_client_verify_X509_certificate(freerdp* instance, const BYTE* data, size_t length,
                                             const char* hostname, UINT16 port, DWORD flags)
{
	pClientContext* pc = nullptr;

	WINPR_ASSERT(instance);
	WINPR_ASSERT(data);
	WINPR_ASSERT(length > 0);
	WINPR_ASSERT(hostname);

	pc = (pClientContext*)instance->context;
	WINPR_ASSERT(pc);

	if (!Stream_EnsureCapacity(pc->remote_pem, length))
		return 0;
	Stream_ResetPosition(pc->remote_pem);

	free(pc->remote_hostname);
	pc->remote_hostname = nullptr;

	if (length > 0)
		Stream_Write(pc->remote_pem, data, length);

	if (hostname)
		pc->remote_hostname = _strdup(hostname);
	pc->remote_port = port;
	pc->remote_flags = flags;

	Stream_SealLength(pc->remote_pem);
	if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_VERIFY_X509, pc->pdata, pc))
		return 0;
	return 1;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_choose_smartcard(WINPR_ATTR_UNUSED freerdp* instance,
                                       WINPR_ATTR_UNUSED SmartcardCertInfo** cert_list,
                                       WINPR_ATTR_UNUSED DWORD count, DWORD* choice,
                                       WINPR_ATTR_UNUSED BOOL gateway)
{
	if (count < 1)
		return FALSE;
	*choice = 0;
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_authenticate_ex(WINPR_ATTR_UNUSED freerdp* instance,
                                      WINPR_ATTR_UNUSED char** username,
                                      WINPR_ATTR_UNUSED char** password,
                                      WINPR_ATTR_UNUSED char** domain, rdp_auth_reason reason)
{
	WINPR_ASSERT(instance);
	WINPR_ASSERT(username);
	WINPR_ASSERT(password);
	WINPR_ASSERT(domain);

	/* Here just return success, if the remote does require some non empty credentials
	 * then it will fail, otherwise a login prompt will be shown. */
	switch (reason)
	{
		case AUTH_RDSTLS:
		case AUTH_NLA:
		case AUTH_TLS:
		case AUTH_RDP:
		case AUTH_SMARTCARD_PIN: /* in this case password is pin code */
		case GW_AUTH_HTTP:
		case GW_AUTH_RDG:
		case GW_AUTH_RPC:
			return TRUE;
		default:
			return FALSE;
	}
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_present_gateway_message(WINPR_ATTR_UNUSED freerdp* instance,
                                              WINPR_ATTR_UNUSED UINT32 type,
                                              WINPR_ATTR_UNUSED BOOL isDisplayMandatory,
                                              WINPR_ATTR_UNUSED BOOL isConsentMandatory,
                                              WINPR_ATTR_UNUSED size_t length,
                                              WINPR_ATTR_UNUSED const WCHAR* message)
{
	WLog_WARN(TAG, "TODO: Implement gateway message forwarding");
	return TRUE;
}

void channel_data_free(void* obj)
{
	union
	{
		const void* cpv;
		void* pv;
	} cnv;
	proxyChannelDataEventInfo* dst = obj;
	if (dst)
	{
		cnv.cpv = dst->data;
		free(cnv.pv);

		cnv.cpv = dst->channel_name;
		free(cnv.pv);
		free(dst);
	}
}

WINPR_ATTR_MALLOC(channel_data_free, 1)
WINPR_ATTR_NODISCARD
static void* channel_data_copy(const void* obj)
{
	union
	{
		const void* cpv;
		void* pv;
	} cnv;
	const proxyChannelDataEventInfo* src = obj;
	proxyChannelDataEventInfo* dst = nullptr;

	WINPR_ASSERT(src);

	dst = calloc(1, sizeof(proxyChannelDataEventInfo));
	if (!dst)
		goto fail;

	*dst = *src;
	if (src->channel_name)
	{
		dst->channel_name = _strdup(src->channel_name);
		if (!dst->channel_name)
			goto fail;
	}
	dst->data = malloc(src->data_len);
	if (!dst->data)
		goto fail;

	cnv.cpv = dst->data;
	memcpy(cnv.pv, src->data, src->data_len);
	return dst;

fail:
	channel_data_free(dst);
	return nullptr;
}

WINPR_ATTR_NODISCARD
static BOOL pf_client_client_new(freerdp* instance, rdpContext* context)
{
	wObject* obj = nullptr;
	pClientContext* pc = (pClientContext*)context;

	if (!instance || !context)
		return FALSE;

	instance->LoadChannels = pf_client_load_channels;
	instance->PreConnect = pf_client_pre_connect;
	instance->PostConnect = pf_client_post_connect;
	instance->PostDisconnect = pf_client_post_disconnect;
	instance->Redirect = pf_client_redirect;
	instance->LogonErrorInfo = pf_logon_error_info;
	instance->GetAccessToken = nullptr;
	instance->RetryDialog = nullptr;
	instance->VerifyCertificateEx = nullptr;
	instance->VerifyChangedCertificateEx = nullptr;
	instance->VerifyX509Certificate = pf_client_verify_X509_certificate;
	instance->AuthenticateEx = pf_client_authenticate_ex;
	instance->ChooseSmartcard = pf_client_choose_smartcard;
	instance->PresentGatewayMessage = pf_client_present_gateway_message;

	pc->remote_pem = Stream_New(nullptr, 4096);
	if (!pc->remote_pem)
		return FALSE;

	pc->sendChannelData = pf_client_send_channel_data;
	pc->cached_server_channel_data = Queue_New(TRUE, -1, -1);
	if (!pc->cached_server_channel_data)
		return FALSE;
	obj = Queue_Object(pc->cached_server_channel_data);
	WINPR_ASSERT(obj);
	obj->fnObjectNew = channel_data_copy;
	obj->fnObjectFree = channel_data_free;

	pc->interceptContextMap = HashTable_New(FALSE);
	if (!pc->interceptContextMap)
		return FALSE;

	if (!HashTable_SetupForStringData(pc->interceptContextMap, FALSE))
		return FALSE;

	obj = HashTable_ValueObject(pc->interceptContextMap);
	WINPR_ASSERT(obj);
	obj->fnObjectFree = intercept_context_entry_free;

	return TRUE;
}

WINPR_ATTR_NODISCARD
static int pf_client_client_stop(rdpContext* context)
{
	pClientContext* pc = (pClientContext*)context;
	proxyData* pdata = nullptr;

	WINPR_ASSERT(pc);
	pdata = pc->pdata;
	WINPR_ASSERT(pdata);

	PROXY_LOG_DBG(TAG, pc, "aborting client connection");
	proxy_data_abort_connect(pdata);
	freerdp_abort_connect_context(context);

	return 0;
}

int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints)
{
	WINPR_ASSERT(pEntryPoints);

	ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS));
	pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION;
	pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1);
	pEntryPoints->ContextSize = sizeof(pClientContext);
	/* Client init and finish */
	pEntryPoints->ClientNew = pf_client_client_new;
	pEntryPoints->ClientFree = pf_client_context_free;
	pEntryPoints->ClientStop = pf_client_client_stop;
	return 0;
}

/**
 * Starts running a client connection towards target server.
 */
DWORD WINAPI pf_client_start(LPVOID arg)
{
	DWORD rc = 1;
	pClientContext* pc = (pClientContext*)arg;

	WINPR_ASSERT(pc);
	if (freerdp_client_start(&pc->context) == 0)
		rc = pf_client_thread_proc(pc);
	freerdp_client_stop(&pc->context);
	return rc;
}
