#include "HyperTwistUX/HyperTwistPlayerSettings.h"

#include "Engine/Engine.h"
#include "GameFramework/GameUserSettings.h"
#include "Framework/Application/SlateApplication.h"
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
#include "Misc/App.h"
#include "Misc/Base64.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/DateTime.h"
#include "VoiceModule.h"
#include "Interfaces/VoiceCapture.h"

#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include <windows.h>
#include <mmreg.h>
#include <dsound.h>
#include <wincrypt.h>
#include "Windows/HideWindowsPlatformTypes.h"
#endif

namespace HyperTwistPlayerSettingsInternal
{
	const TCHAR* PreferencesSection = TEXT("HyperTwist.PlayerPreferences");
	const TCHAR* BindingSection = TEXT("HyperTwist.PlayerKeyBindings");
	const TCHAR* AccountSection = TEXT("HyperTwist.DesktopAccount");
	const TCHAR* CredentialSection = TEXT("HyperTwist.ProtectedCredentials");
	const TCHAR* CredentialEntropy = TEXT("HyperTwist/provider-credential/v1");

	const TCHAR* MainMenuMapAssetPath =
		TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining");
	const TCHAR* MainMenuGameModeClassPath =
		TEXT("/Script/UnrealHyperTwist.HyperTwistFirstRunLaunchGameMode");
	const TCHAR* AccountWebsiteUrl = TEXT("https://hypertwist.app/app/account");
	const TCHAR* DesktopLinkVerifyUrl =
		TEXT("https://hypertwist.app/api/auth/desktop-link/verify");
	const TCHAR* WcaKeyboardProfileId = TEXT("classic-wca-keyboard/v1");
	const TCHAR* MnemonicKeyboardProfileId = TEXT("classic-mnemonic-pairs/v1");
	const TCHAR* CustomKeyboardProfileId = TEXT("custom/v1");

	struct FBindingDefinition
	{
		FName ActionId;
		const TCHAR* Label;
		const TCHAR* Category;
		FKey DefaultKey;
	};

	const TArray<FBindingDefinition>& GetBindingDefinitions()
	{
		static const TArray<FBindingDefinition> Definitions = {
			{TEXT("navigation.pause"), TEXT("Pause / Back"), TEXT("Navigation"), EKeys::Escape},
			{TEXT("navigation.main-menu"), TEXT("Main Menu"), TEXT("Navigation"), EKeys::F10},
			{TEXT("puzzle.new"), TEXT("New Puzzle"), TEXT("Puzzle"), EKeys::R},
			{TEXT("puzzle.hint"), TEXT("Request Hint"), TEXT("Puzzle"), EKeys::F1},
			{TEXT("puzzle.submit"), TEXT("Check / Submit"), TEXT("Puzzle"), EKeys::Enter},
			{TEXT("lesson.toggle"), TEXT("Toggle Guided Mode"), TEXT("Learning"), EKeys::Tab},
			{TEXT("assistant.toggle"), TEXT("Open / Close Coach"), TEXT("Coach"), EKeys::F3},
			{TEXT("speech.push-to-talk"), TEXT("Push To Talk"), TEXT("Speech"), EKeys::V},
			{TEXT("speech.cycle-voice"), TEXT("Cycle Voice"), TEXT("Speech"), EKeys::C},
			{TEXT("classic.R.cw"), TEXT("R"), TEXT("Classic moves"), EKeys::I},
			{TEXT("classic.R.ccw"), TEXT("R'"), TEXT("Classic moves"), EKeys::K},
			{TEXT("classic.U.cw"), TEXT("U"), TEXT("Classic moves"), EKeys::J},
			{TEXT("classic.U.ccw"), TEXT("U'"), TEXT("Classic moves"), EKeys::F},
			{TEXT("classic.F.cw"), TEXT("F"), TEXT("Classic moves"), EKeys::H},
			{TEXT("classic.F.ccw"), TEXT("F'"), TEXT("Classic moves"), EKeys::G},
			{TEXT("classic.L.cw"), TEXT("L"), TEXT("Classic moves"), EKeys::D},
			{TEXT("classic.L.ccw"), TEXT("L'"), TEXT("Classic moves"), EKeys::E},
			{TEXT("classic.D.cw"), TEXT("D"), TEXT("Classic moves"), EKeys::S},
			{TEXT("classic.D.ccw"), TEXT("D'"), TEXT("Classic moves"), EKeys::L},
			{TEXT("classic.B.cw"), TEXT("B"), TEXT("Classic moves"), EKeys::W},
			{TEXT("classic.B.ccw"), TEXT("B'"), TEXT("Classic moves"), EKeys::O},
			{TEXT("classic.x.cw"), TEXT("Rotate X"), TEXT("Classic rotations"), EKeys::T},
			{TEXT("classic.x.ccw"), TEXT("Rotate X'"), TEXT("Classic rotations"), EKeys::Y},
			{TEXT("classic.y.cw"), TEXT("Rotate Y"), TEXT("Classic rotations"), EKeys::N},
			{TEXT("classic.y.ccw"), TEXT("Rotate Y'"), TEXT("Classic rotations"), EKeys::B},
			{TEXT("classic.z.cw"), TEXT("Rotate Z"), TEXT("Classic rotations"), EKeys::P},
			{TEXT("classic.z.ccw"), TEXT("Rotate Z'"), TEXT("Classic rotations"), EKeys::Q}
		};
		return Definitions;
	}

	const TMap<FName, FKey>& GetWcaBindings()
	{
		static const TMap<FName, FKey> Bindings = {
			{TEXT("classic.R.cw"), EKeys::I},
			{TEXT("classic.R.ccw"), EKeys::K},
			{TEXT("classic.U.cw"), EKeys::J},
			{TEXT("classic.U.ccw"), EKeys::F},
			{TEXT("classic.F.cw"), EKeys::H},
			{TEXT("classic.F.ccw"), EKeys::G},
			{TEXT("classic.L.cw"), EKeys::D},
			{TEXT("classic.L.ccw"), EKeys::E},
			{TEXT("classic.D.cw"), EKeys::S},
			{TEXT("classic.D.ccw"), EKeys::L},
			{TEXT("classic.B.cw"), EKeys::W},
			{TEXT("classic.B.ccw"), EKeys::O},
			{TEXT("classic.x.cw"), EKeys::T},
			{TEXT("classic.x.ccw"), EKeys::Y},
			{TEXT("classic.y.cw"), EKeys::N},
			{TEXT("classic.y.ccw"), EKeys::B},
			{TEXT("classic.z.cw"), EKeys::P},
			{TEXT("classic.z.ccw"), EKeys::Q}
		};
		return Bindings;
	}

	const TMap<FName, FKey>& GetMnemonicBindings()
	{
		static const TMap<FName, FKey> Bindings = {
			{TEXT("classic.R.cw"), EKeys::R},
			{TEXT("classic.R.ccw"), EKeys::T},
			{TEXT("classic.U.cw"), EKeys::U},
			{TEXT("classic.U.ccw"), EKeys::Y},
			{TEXT("classic.F.cw"), EKeys::F},
			{TEXT("classic.F.ccw"), EKeys::G},
			{TEXT("classic.L.cw"), EKeys::L},
			{TEXT("classic.L.ccw"), EKeys::K},
			{TEXT("classic.D.cw"), EKeys::D},
			{TEXT("classic.D.ccw"), EKeys::S},
			{TEXT("classic.B.cw"), EKeys::B},
			{TEXT("classic.B.ccw"), EKeys::N},
			{TEXT("classic.x.cw"), EKeys::X},
			{TEXT("classic.x.ccw"), EKeys::Z},
			{TEXT("classic.y.cw"), EKeys::C},
			{TEXT("classic.y.ccw"), EKeys::M},
			{TEXT("classic.z.cw"), EKeys::A},
			{TEXT("classic.z.ccw"), EKeys::Q}
		};
		return Bindings;
	}

	bool IsSupportedKeyboardProfile(const FString& ProfileId)
	{
		return ProfileId == WcaKeyboardProfileId
			|| ProfileId == MnemonicKeyboardProfileId
			|| ProfileId == CustomKeyboardProfileId;
	}

	void ApplyProfileBindings(
		FHyperTwistPlayerPreferences& Preferences,
		const TMap<FName, FKey>& ProfileBindings
	)
	{
		for (const TPair<FName, FKey>& Binding : ProfileBindings)
		{
			Preferences.KeyBindings.Add(Binding.Key, Binding.Value);
		}
	}

	const FBindingDefinition* FindBindingDefinition(const FName ActionId)
	{
		return GetBindingDefinitions().FindByPredicate(
			[ActionId](const FBindingDefinition& Definition)
			{
				return Definition.ActionId == ActionId;
			});
	}

	FString GetConfigString(
		const TCHAR* Section,
		const TCHAR* Key,
		const FString& DefaultValue
	)
	{
		FString Value = DefaultValue;
		if (GConfig != nullptr)
		{
			GConfig->GetString(Section, Key, Value, GGameUserSettingsIni);
		}
		return Value;
	}

	void SetConfigString(const TCHAR* Section, const TCHAR* Key, const FString& Value)
	{
		if (GConfig != nullptr)
		{
			GConfig->SetString(Section, Key, *Value, GGameUserSettingsIni);
		}
	}

	bool IsCredentialSlotAllowed(const FString& CredentialSlot)
	{
		if (CredentialSlot.IsEmpty() || CredentialSlot.Len() > 64)
		{
			return false;
		}

		for (const TCHAR Character : CredentialSlot)
		{
			if (!FChar::IsAlnum(Character) && Character != TEXT('-') && Character != TEXT('_'))
			{
				return false;
			}
		}
		return true;
	}

	FString TrimAndLimit(const FString& Value, const int32 MaximumLength)
	{
		const FString Trimmed = Value.TrimStartAndEnd();
		return Trimmed.Len() <= MaximumLength
			? Trimmed
			: Trimmed.Left(MaximumLength);
	}

	bool HasUrlHostBoundary(const FString& Url, const FString& Prefix)
	{
		if (!Url.StartsWith(Prefix, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (Url.Len() == Prefix.Len())
		{
			return true;
		}
		if (Url.Len() < Prefix.Len())
		{
			return false;
		}

		const TCHAR NextCharacter = Url[Prefix.Len()];
		return NextCharacter == TEXT(':')
			|| NextCharacter == TEXT('/')
			|| NextCharacter == TEXT('?')
			|| NextCharacter == TEXT('#');
	}

	bool IsLoopbackHttpEndpoint(const FString& Endpoint)
	{
		return HasUrlHostBoundary(Endpoint, TEXT("http://localhost"))
			|| HasUrlHostBoundary(Endpoint, TEXT("http://127.0.0.1"))
			|| HasUrlHostBoundary(Endpoint, TEXT("http://[::1]"))
			|| HasUrlHostBoundary(Endpoint, TEXT("https://localhost"))
			|| HasUrlHostBoundary(Endpoint, TEXT("https://127.0.0.1"))
			|| HasUrlHostBoundary(Endpoint, TEXT("https://[::1]"));
	}

	FHyperTwistPlayerPreferences SanitizePreferences(
		const FHyperTwistPlayerPreferences& Candidate
	)
	{
		FHyperTwistPlayerPreferences Result = Candidate;
			Result.SchemaVersion = 4;
		if (!IsSupportedKeyboardProfile(Result.KeyboardProfileId))
		{
			Result.KeyboardProfileId = WcaKeyboardProfileId;
		}
		Result.PointerSensitivity = FMath::Clamp(Result.PointerSensitivity, 0.2f, 3.0f);
		Result.OrbitSensitivity = FMath::Clamp(Result.OrbitSensitivity, 0.2f, 3.0f);
		Result.ZoomSensitivity = FMath::Clamp(Result.ZoomSensitivity, 0.2f, 3.0f);
		Result.MasterVolume = FMath::Clamp(Result.MasterVolume, 0.0f, 1.0f);
		Result.MusicVolume = FMath::Clamp(Result.MusicVolume, 0.0f, 1.0f);
		Result.EffectsVolume = FMath::Clamp(Result.EffectsVolume, 0.0f, 1.0f);
		Result.VoiceVolume = FMath::Clamp(Result.VoiceVolume, 0.0f, 1.0f);
		Result.ResolutionScalePercent =
			FMath::Clamp(Result.ResolutionScalePercent, 50.0f, 100.0f);
		Result.UiScale = FMath::Clamp(Result.UiScale, 0.8f, 1.4f);
		Result.SpeechProviderId = TrimAndLimit(Result.SpeechProviderId, 64);
		Result.SpeechEndpoint = TrimAndLimit(Result.SpeechEndpoint, 2048);
			Result.SpeechActivationMode = Result.SpeechActivationMode.Equals(
				TEXT("toggle"),
				ESearchCase::IgnoreCase)
					? TEXT("toggle")
					: TEXT("hold");
			Result.SpeechDictationDestination =
				Result.SpeechDictationDestination.Equals(
					TEXT("coach-send"),
					ESearchCase::IgnoreCase)
						? TEXT("coach-send")
						: TEXT("coach-draft");
		Result.SpeechModel = TrimAndLimit(Result.SpeechModel, 256);
		Result.SpeechLanguage = TrimAndLimit(Result.SpeechLanguage, 32);
		if (Result.SpeechLanguage.IsEmpty())
		{
			Result.SpeechLanguage = TEXT("auto");
		}
		Result.SpeechMicrophoneId = TrimAndLimit(Result.SpeechMicrophoneId, 512);
		if (Result.SpeechMicrophoneId.IsEmpty())
		{
			Result.SpeechMicrophoneId = TEXT("system-default");
		}
		Result.SpeechRecognitionContext =
			TrimAndLimit(Result.SpeechRecognitionContext, 2000);
		Result.VoiceProviderId = TrimAndLimit(Result.VoiceProviderId, 64);
		Result.VoiceEndpoint = TrimAndLimit(Result.VoiceEndpoint, 2048);
		Result.VoiceModel = TrimAndLimit(Result.VoiceModel, 256);
		Result.VoiceId = TrimAndLimit(Result.VoiceId, 256);
		Result.VoiceSpeed = FMath::Clamp(Result.VoiceSpeed, 0.5f, 2.0f);
		Result.CoachProviderId = TrimAndLimit(Result.CoachProviderId, 64);
		Result.CoachEndpoint = TrimAndLimit(Result.CoachEndpoint, 2048);
		Result.CoachModel = TrimAndLimit(Result.CoachModel, 256);
		Result.CoachSystemInstructions =
			TrimAndLimit(Result.CoachSystemInstructions, 4000);
		Result.CoachTemperature = FMath::Clamp(Result.CoachTemperature, 0.0f, 1.0f);
		Result.CoachMaxResponseTokens =
			FMath::Clamp(Result.CoachMaxResponseTokens, 128, 2048);
		Result.CoachRequestTimeoutSeconds =
			FMath::Clamp(Result.CoachRequestTimeoutSeconds, 5.0f, 60.0f);
		if (Result.CoachProviderId.IsEmpty()
			|| Result.CoachProviderId.Equals(TEXT("local-disabled"), ESearchCase::IgnoreCase))
		{
			Result.bCoachAiEnabled = false;
		}

		for (const FBindingDefinition& Definition : GetBindingDefinitions())
		{
			const FKey* ExistingKey = Result.KeyBindings.Find(Definition.ActionId);
			if (ExistingKey == nullptr || !ExistingKey->IsValid())
			{
				Result.KeyBindings.Add(Definition.ActionId, Definition.DefaultKey);
			}
		}

		return Result;
	}

	#if PLATFORM_WINDOWS
		BOOL CALLBACK EnumerateCaptureDevice(
			LPGUID DeviceGuid,
			LPCWSTR DeviceDescription,
			LPCWSTR DriverModule,
			LPVOID Context
		)
		{
			static_cast<void>(DriverModule);
			if (DeviceGuid == nullptr
				|| DeviceDescription == nullptr
				|| Context == nullptr)
			{
				return static_cast<BOOL>(true);
			}

			TArray<FHyperTwistMicrophoneDescriptor>& Microphones =
				*static_cast<TArray<FHyperTwistMicrophoneDescriptor>*>(Context);
			const FString Description(DeviceDescription);
			if (Description.IsEmpty()
				|| Microphones.ContainsByPredicate(
					[&Description](const FHyperTwistMicrophoneDescriptor& Existing)
					{
						return Existing.DeviceId.Equals(
							Description,
							ESearchCase::IgnoreCase);
					}))
			{
				return static_cast<BOOL>(true);
			}

			FHyperTwistMicrophoneDescriptor Descriptor;
			Descriptor.DeviceId = Description;
			Descriptor.DisplayName = Description;
			Descriptor.bIsDefault = false;
			Microphones.Add(MoveTemp(Descriptor));
			return static_cast<BOOL>(true);
		}

		bool ProtectString(const FString& PlainText, FString& OutProtectedBase64)
	{
		FTCHARToUTF8 PlainUtf8(*PlainText);
		FTCHARToUTF8 EntropyUtf8(CredentialEntropy);
		DATA_BLOB InputBlob;
		InputBlob.cbData = static_cast<DWORD>(PlainUtf8.Length());
		InputBlob.pbData = reinterpret_cast<BYTE*>(
			const_cast<ANSICHAR*>(PlainUtf8.Get()));
		DATA_BLOB EntropyBlob;
		EntropyBlob.cbData = static_cast<DWORD>(EntropyUtf8.Length());
		EntropyBlob.pbData = reinterpret_cast<BYTE*>(
			const_cast<ANSICHAR*>(EntropyUtf8.Get()));
		DATA_BLOB OutputBlob = {};

		if (!CryptProtectData(
				&InputBlob,
				L"HyperTwist provider credential",
				&EntropyBlob,
				nullptr,
				nullptr,
				CRYPTPROTECT_UI_FORBIDDEN,
				&OutputBlob))
		{
			return false;
		}

		OutProtectedBase64 = FBase64::Encode(OutputBlob.pbData, OutputBlob.cbData);
		LocalFree(OutputBlob.pbData);
		return true;
	}

	bool UnprotectString(const FString& ProtectedBase64, FString& OutPlainText)
	{
		TArray<uint8> ProtectedBytes;
		if (!FBase64::Decode(ProtectedBase64, ProtectedBytes) || ProtectedBytes.IsEmpty())
		{
			return false;
		}

		FTCHARToUTF8 EntropyUtf8(CredentialEntropy);
		DATA_BLOB InputBlob;
		InputBlob.cbData = static_cast<DWORD>(ProtectedBytes.Num());
		InputBlob.pbData = ProtectedBytes.GetData();
		DATA_BLOB EntropyBlob;
		EntropyBlob.cbData = static_cast<DWORD>(EntropyUtf8.Length());
		EntropyBlob.pbData = reinterpret_cast<BYTE*>(
			const_cast<ANSICHAR*>(EntropyUtf8.Get()));
		DATA_BLOB OutputBlob = {};

		if (!CryptUnprotectData(
				&InputBlob,
				nullptr,
				&EntropyBlob,
				nullptr,
				nullptr,
				CRYPTPROTECT_UI_FORBIDDEN,
				&OutputBlob))
		{
			return false;
		}

		FUTF8ToTCHAR PlainText(
			reinterpret_cast<const ANSICHAR*>(OutputBlob.pbData),
			static_cast<int32>(OutputBlob.cbData));
		OutPlainText = FString(PlainText.Length(), PlainText.Get());
		LocalFree(OutputBlob.pbData);
		return true;
	}
#endif
}

bool FHyperTwistPlayerPreferences::IsStructurallyValid() const
{
	return SchemaVersion == 4
		&& !KeyboardProfileId.IsEmpty()
		&& PointerSensitivity >= 0.2f
		&& OrbitSensitivity >= 0.2f
		&& ZoomSensitivity >= 0.2f
		&& MasterVolume >= 0.0f
		&& MasterVolume <= 1.0f
		&& MusicVolume >= 0.0f
		&& MusicVolume <= 1.0f
		&& EffectsVolume >= 0.0f
		&& EffectsVolume <= 1.0f
		&& VoiceVolume >= 0.0f
		&& VoiceVolume <= 1.0f
		&& ResolutionScalePercent >= 50.0f
		&& ResolutionScalePercent <= 100.0f
		&& UiScale >= 0.8f
		&& UiScale <= 1.4f
		&& !SpeechActivationMode.IsEmpty()
		&& !SpeechDictationDestination.IsEmpty()
		&& !SpeechLanguage.IsEmpty()
		&& VoiceSpeed >= 0.5f
		&& VoiceSpeed <= 2.0f
		&& CoachTemperature >= 0.0f
		&& CoachTemperature <= 1.0f
		&& CoachMaxResponseTokens >= 128
		&& CoachMaxResponseTokens <= 2048
		&& CoachRequestTimeoutSeconds >= 5.0f
		&& CoachRequestTimeoutSeconds <= 60.0f;
}

FHyperTwistPlayerPreferences UHyperTwistPlayerSettingsLibrary::GetDefaultPreferences()
{
	return HyperTwistPlayerSettingsInternal::SanitizePreferences(
		FHyperTwistPlayerPreferences());
}

FHyperTwistPlayerPreferences UHyperTwistPlayerSettingsLibrary::LoadPreferences()
{
	using namespace HyperTwistPlayerSettingsInternal;

	FHyperTwistPlayerPreferences Result = GetDefaultPreferences();
	if (GConfig == nullptr)
	{
		return Result;
	}

	GConfig->GetInt(PreferencesSection, TEXT("SchemaVersion"), Result.SchemaVersion, GGameUserSettingsIni);
	GConfig->GetString(
		PreferencesSection,
		TEXT("KeyboardProfileId"),
		Result.KeyboardProfileId,
		GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("PointerSensitivity"), Result.PointerSensitivity, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("OrbitSensitivity"), Result.OrbitSensitivity, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("ZoomSensitivity"), Result.ZoomSensitivity, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("InvertOrbitY"), Result.bInvertOrbitY, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("TouchInputEnabled"), Result.bTouchInputEnabled, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("MasterVolume"), Result.MasterVolume, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("MusicVolume"), Result.MusicVolume, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("EffectsVolume"), Result.EffectsVolume, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("VoiceVolume"), Result.VoiceVolume, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("Muted"), Result.bMuted, GGameUserSettingsIni);
	int32 GraphicsQuality = static_cast<int32>(Result.GraphicsQuality);
	GConfig->GetInt(PreferencesSection, TEXT("GraphicsQuality"), GraphicsQuality, GGameUserSettingsIni);
	Result.GraphicsQuality = static_cast<EHyperTwistGraphicsQuality>(
		FMath::Clamp(GraphicsQuality, 0, 3));
	int32 WindowMode = static_cast<int32>(Result.WindowMode);
	GConfig->GetInt(PreferencesSection, TEXT("WindowMode"), WindowMode, GGameUserSettingsIni);
	Result.WindowMode = static_cast<EHyperTwistWindowMode>(FMath::Clamp(WindowMode, 0, 2));
	GConfig->GetFloat(
		PreferencesSection,
		TEXT("ResolutionScalePercent"),
		Result.ResolutionScalePercent,
		GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("VSyncEnabled"), Result.bVSyncEnabled, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("UiScale"), Result.UiScale, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("ReducedMotion"), Result.bReducedMotion, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("HighContrast"), Result.bHighContrast, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("SubtitlesEnabled"), Result.bSubtitlesEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("SpeechInputEnabled"), Result.bSpeechInputEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("CoachNarrationEnabled"), Result.bCoachNarrationEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("AllowCloudProviders"), Result.bAllowCloudProviders, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechProviderId"), Result.SpeechProviderId, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechEndpoint"), Result.SpeechEndpoint, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechActivationMode"), Result.SpeechActivationMode, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechDictationDestination"), Result.SpeechDictationDestination, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechModel"), Result.SpeechModel, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechLanguage"), Result.SpeechLanguage, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechMicrophoneId"), Result.SpeechMicrophoneId, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("SpeechRecognitionContext"), Result.SpeechRecognitionContext, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("SpeechCleanupEnabled"), Result.bSpeechCleanupEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("SpeechSoundFeedbackEnabled"), Result.bSpeechSoundFeedbackEnabled, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("VoiceProviderId"), Result.VoiceProviderId, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("VoiceEndpoint"), Result.VoiceEndpoint, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("VoiceModel"), Result.VoiceModel, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("VoiceId"), Result.VoiceId, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("VoiceSpeed"), Result.VoiceSpeed, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("CoachAiEnabled"), Result.bCoachAiEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("AssistantPanelEnabled"), Result.bAssistantPanelEnabled, GGameUserSettingsIni);
	GConfig->GetBool(PreferencesSection, TEXT("AssistantPanelOpenByDefault"), Result.bAssistantPanelOpenByDefault, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("CoachProviderId"), Result.CoachProviderId, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("CoachEndpoint"), Result.CoachEndpoint, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("CoachModel"), Result.CoachModel, GGameUserSettingsIni);
	GConfig->GetString(PreferencesSection, TEXT("CoachSystemInstructions"), Result.CoachSystemInstructions, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("CoachTemperature"), Result.CoachTemperature, GGameUserSettingsIni);
	GConfig->GetInt(PreferencesSection, TEXT("CoachMaxResponseTokens"), Result.CoachMaxResponseTokens, GGameUserSettingsIni);
	GConfig->GetFloat(PreferencesSection, TEXT("CoachRequestTimeoutSeconds"), Result.CoachRequestTimeoutSeconds, GGameUserSettingsIni);

	for (const FBindingDefinition& Definition : GetBindingDefinitions())
	{
		FString KeyName;
		if (GConfig->GetString(
				BindingSection,
				*Definition.ActionId.ToString(),
				KeyName,
				GGameUserSettingsIni))
		{
			const FKey ConfiguredKey{FName(*KeyName)};
			if (ConfiguredKey.IsValid())
			{
				Result.KeyBindings.Add(Definition.ActionId, ConfiguredKey);
			}
		}
	}

	return SanitizePreferences(Result);
}

bool UHyperTwistPlayerSettingsLibrary::SavePreferences(
	const FHyperTwistPlayerPreferences& Preferences
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	if (GConfig == nullptr)
	{
		return false;
	}

	const FHyperTwistPlayerPreferences Safe = SanitizePreferences(Preferences);
	GConfig->SetInt(PreferencesSection, TEXT("SchemaVersion"), Safe.SchemaVersion, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("KeyboardProfileId"), *Safe.KeyboardProfileId, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("PointerSensitivity"), Safe.PointerSensitivity, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("OrbitSensitivity"), Safe.OrbitSensitivity, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("ZoomSensitivity"), Safe.ZoomSensitivity, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("InvertOrbitY"), Safe.bInvertOrbitY, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("TouchInputEnabled"), Safe.bTouchInputEnabled, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("MasterVolume"), Safe.MasterVolume, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("MusicVolume"), Safe.MusicVolume, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("EffectsVolume"), Safe.EffectsVolume, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("VoiceVolume"), Safe.VoiceVolume, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("Muted"), Safe.bMuted, GGameUserSettingsIni);
	GConfig->SetInt(PreferencesSection, TEXT("GraphicsQuality"), static_cast<int32>(Safe.GraphicsQuality), GGameUserSettingsIni);
	GConfig->SetInt(PreferencesSection, TEXT("WindowMode"), static_cast<int32>(Safe.WindowMode), GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("ResolutionScalePercent"), Safe.ResolutionScalePercent, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("VSyncEnabled"), Safe.bVSyncEnabled, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("UiScale"), Safe.UiScale, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("ReducedMotion"), Safe.bReducedMotion, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("HighContrast"), Safe.bHighContrast, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("SubtitlesEnabled"), Safe.bSubtitlesEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("SpeechInputEnabled"), Safe.bSpeechInputEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("CoachNarrationEnabled"), Safe.bCoachNarrationEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("AllowCloudProviders"), Safe.bAllowCloudProviders, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechProviderId"), *Safe.SpeechProviderId, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechEndpoint"), *Safe.SpeechEndpoint, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechActivationMode"), *Safe.SpeechActivationMode, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechDictationDestination"), *Safe.SpeechDictationDestination, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechModel"), *Safe.SpeechModel, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechLanguage"), *Safe.SpeechLanguage, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechMicrophoneId"), *Safe.SpeechMicrophoneId, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("SpeechRecognitionContext"), *Safe.SpeechRecognitionContext, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("SpeechCleanupEnabled"), Safe.bSpeechCleanupEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("SpeechSoundFeedbackEnabled"), Safe.bSpeechSoundFeedbackEnabled, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("VoiceProviderId"), *Safe.VoiceProviderId, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("VoiceEndpoint"), *Safe.VoiceEndpoint, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("VoiceModel"), *Safe.VoiceModel, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("VoiceId"), *Safe.VoiceId, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("VoiceSpeed"), Safe.VoiceSpeed, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("CoachAiEnabled"), Safe.bCoachAiEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("AssistantPanelEnabled"), Safe.bAssistantPanelEnabled, GGameUserSettingsIni);
	GConfig->SetBool(PreferencesSection, TEXT("AssistantPanelOpenByDefault"), Safe.bAssistantPanelOpenByDefault, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("CoachProviderId"), *Safe.CoachProviderId, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("CoachEndpoint"), *Safe.CoachEndpoint, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("CoachModel"), *Safe.CoachModel, GGameUserSettingsIni);
	GConfig->SetString(PreferencesSection, TEXT("CoachSystemInstructions"), *Safe.CoachSystemInstructions, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("CoachTemperature"), Safe.CoachTemperature, GGameUserSettingsIni);
	GConfig->SetInt(PreferencesSection, TEXT("CoachMaxResponseTokens"), Safe.CoachMaxResponseTokens, GGameUserSettingsIni);
	GConfig->SetFloat(PreferencesSection, TEXT("CoachRequestTimeoutSeconds"), Safe.CoachRequestTimeoutSeconds, GGameUserSettingsIni);

	for (const FBindingDefinition& Definition : GetBindingDefinitions())
	{
		const FKey Key = ResolveKeyBinding(Safe, Definition.ActionId);
		GConfig->SetString(
			BindingSection,
			*Definition.ActionId.ToString(),
			*Key.GetFName().ToString(),
			GGameUserSettingsIni);
	}

	GConfig->Flush(false, GGameUserSettingsIni);
	return true;
}

void UHyperTwistPlayerSettingsLibrary::ApplyRuntimePreferences(
	const FHyperTwistPlayerPreferences& Preferences,
	const bool bApplyGraphicsSettings
)
{
	const FHyperTwistPlayerPreferences Safe =
		HyperTwistPlayerSettingsInternal::SanitizePreferences(Preferences);
	FApp::SetVolumeMultiplier(Safe.bMuted ? 0.0f : Safe.MasterVolume);
	UHyperTwistSpeechLibrary::ApplyManagedAudioCategoryVolumes(
		Safe.MusicVolume,
		Safe.EffectsVolume,
		Safe.VoiceVolume);

	if (FSlateApplication::IsInitialized())
	{
		FSlateApplication::Get().SetApplicationScale(Safe.UiScale);
	}

	if (!bApplyGraphicsSettings || GEngine == nullptr)
	{
		return;
	}

	UGameUserSettings* GameUserSettings = GEngine->GetGameUserSettings();
	if (GameUserSettings == nullptr)
	{
		return;
	}

	GameUserSettings->SetOverallScalabilityLevel(static_cast<int32>(Safe.GraphicsQuality));
	GameUserSettings->SetResolutionScaleValueEx(Safe.ResolutionScalePercent);
	GameUserSettings->SetVSyncEnabled(Safe.bVSyncEnabled);
	switch (Safe.WindowMode)
	{
	case EHyperTwistWindowMode::Fullscreen:
		GameUserSettings->SetFullscreenMode(EWindowMode::Fullscreen);
		break;
	case EHyperTwistWindowMode::Windowed:
		GameUserSettings->SetFullscreenMode(EWindowMode::Windowed);
		break;
	default:
		GameUserSettings->SetFullscreenMode(EWindowMode::WindowedFullscreen);
		break;
	}
	GameUserSettings->ApplySettings(false);
	GameUserSettings->SaveSettings();
}

TArray<FHyperTwistKeyBindingDescriptor>
UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(
	const FHyperTwistPlayerPreferences& Preferences
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	const FHyperTwistPlayerPreferences Safe = SanitizePreferences(Preferences);
	TArray<FHyperTwistKeyBindingDescriptor> Result;
	Result.Reserve(GetBindingDefinitions().Num());
	for (const FBindingDefinition& Definition : GetBindingDefinitions())
	{
		FHyperTwistKeyBindingDescriptor Descriptor;
		Descriptor.ActionId = Definition.ActionId;
		Descriptor.Label = Definition.Label;
		Descriptor.Category = Definition.Category;
		Descriptor.Key = ResolveKeyBinding(Safe, Definition.ActionId);
		Result.Add(Descriptor);
	}
	return Result;
}

TArray<FString> UHyperTwistPlayerSettingsLibrary::GetSupportedKeyboardProfileIds()
{
	using namespace HyperTwistPlayerSettingsInternal;
	return {
		WcaKeyboardProfileId,
		MnemonicKeyboardProfileId,
		CustomKeyboardProfileId
	};
}

FString UHyperTwistPlayerSettingsLibrary::GetKeyboardProfileDisplayName(
	const FString& ProfileId
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	if (ProfileId == WcaKeyboardProfileId)
	{
		return TEXT("WCA Pro");
	}
	if (ProfileId == MnemonicKeyboardProfileId)
	{
		return TEXT("Mnemonic Pairs");
	}
	if (ProfileId == CustomKeyboardProfileId)
	{
		return TEXT("Custom");
	}
	return TEXT("Unknown");
}

bool UHyperTwistPlayerSettingsLibrary::ApplyKeyboardProfile(
	FHyperTwistPlayerPreferences& Preferences,
	const FString& ProfileId
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	if (!IsSupportedKeyboardProfile(ProfileId))
	{
		return false;
	}

	if (ProfileId != CustomKeyboardProfileId)
	{
		Preferences.KeyBindings.Reset();
		for (const FBindingDefinition& Definition : GetBindingDefinitions())
		{
			Preferences.KeyBindings.Add(Definition.ActionId, Definition.DefaultKey);
		}
	}

	if (ProfileId == WcaKeyboardProfileId)
	{
		ApplyProfileBindings(Preferences, GetWcaBindings());
		Preferences.KeyBindings.Add(TEXT("puzzle.new"), EKeys::R);
		Preferences.KeyBindings.Add(TEXT("speech.cycle-voice"), EKeys::C);
	}
	else if (ProfileId == MnemonicKeyboardProfileId)
	{
		ApplyProfileBindings(Preferences, GetMnemonicBindings());
		Preferences.KeyBindings.Add(TEXT("puzzle.new"), EKeys::F2);
		Preferences.KeyBindings.Add(TEXT("speech.cycle-voice"), EKeys::F4);
	}
	Preferences.KeyboardProfileId = ProfileId;
	return true;
}

bool UHyperTwistPlayerSettingsLibrary::TrySetKeyBinding(
	FHyperTwistPlayerPreferences& Preferences,
	const FName ActionId,
	const FKey NewKey,
	FString& OutFailureReason
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	OutFailureReason.Reset();
	if (FindBindingDefinition(ActionId) == nullptr)
	{
		OutFailureReason = TEXT("Unknown HyperTwist action.");
		return false;
	}
	if (!NewKey.IsValid()
		|| NewKey.IsGamepadKey()
		|| NewKey.GetFName().ToString().StartsWith(TEXT("Touch")))
	{
		OutFailureReason = TEXT("Choose a valid keyboard or mouse key.");
		return false;
	}

	const FKey PreviousKey = ResolveKeyBinding(Preferences, ActionId);
	for (TPair<FName, FKey>& ExistingBinding : Preferences.KeyBindings)
	{
		if (ExistingBinding.Key != ActionId && ExistingBinding.Value == NewKey)
		{
			ExistingBinding.Value = PreviousKey;
			break;
		}
	}

	Preferences.KeyBindings.Add(ActionId, NewKey);
	Preferences.KeyboardProfileId = CustomKeyboardProfileId;
	return true;
}

FKey UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
	const FHyperTwistPlayerPreferences& Preferences,
	const FName ActionId
)
{
	if (const FKey* ConfiguredKey = Preferences.KeyBindings.Find(ActionId);
		ConfiguredKey != nullptr && ConfiguredKey->IsValid())
	{
		return *ConfiguredKey;
	}
	if (const HyperTwistPlayerSettingsInternal::FBindingDefinition* Definition =
			HyperTwistPlayerSettingsInternal::FindBindingDefinition(ActionId))
	{
		return Definition->DefaultKey;
	}
	return EKeys::Invalid;
}

FName UHyperTwistPlayerSettingsLibrary::BuildClassicMoveBindingId(
	const FHyperTwistAlgorithmBlockMove& Move
)
{
	if (Move.Family.IsEmpty() || Move.Amount == 0)
	{
		return NAME_None;
	}
	return FName(*FString::Printf(
		TEXT("classic.%s.%s"),
		*Move.Family,
		Move.Amount < 0 ? TEXT("ccw") : TEXT("cw")));
}

FHyperTwistAlgorithmKeyboardProfile
UHyperTwistPlayerSettingsLibrary::BuildClassicKeyboardProfile(
	const FHyperTwistPlayerPreferences& Preferences
)
{
	FHyperTwistAlgorithmKeyboardProfile Profile =
		UHyperTwistAlgorithmKeyboardLibrary::GetClassicWcaKeyboardProfile();
	for (FHyperTwistAlgorithmKeyboardBinding& Binding : Profile.Bindings)
	{
		const FName BindingId = BuildClassicMoveBindingId(Binding.Move);
		if (!BindingId.IsNone())
		{
			Binding.Key = ResolveKeyBinding(Preferences, BindingId);
		}
	}
	return Profile;
}

TArray<FHyperTwistMicrophoneDescriptor>
UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones()
{
	TArray<FHyperTwistMicrophoneDescriptor> Microphones;
	FHyperTwistMicrophoneDescriptor SystemDefault;
	SystemDefault.bIsDefault = true;
	Microphones.Add(SystemDefault);

#if PLATFORM_WINDOWS
	DirectSoundCaptureEnumerateW(
		HyperTwistPlayerSettingsInternal::EnumerateCaptureDevice,
		&Microphones);
#endif

	return Microphones;
}

bool UHyperTwistPlayerSettingsLibrary::TestMicrophoneDevice(
	const FString& DeviceId,
	FString& OutFailureReason
)
{
	OutFailureReason.Reset();
	if (!FVoiceModule::Get().DoesPlatformSupportVoiceCapture())
	{
		OutFailureReason = TEXT("Microphone capture is unsupported on this platform.");
		return false;
	}

	const FString DeviceName = DeviceId.Equals(
		TEXT("system-default"),
		ESearchCase::IgnoreCase)
		? FString()
		: DeviceId;
	TSharedPtr<IVoiceCapture> Capture =
		FVoiceModule::Get().CreateVoiceCapture(DeviceName, 16000, 1);
	if (!Capture.IsValid())
	{
		OutFailureReason =
			TEXT("The selected microphone is unavailable. Refresh devices or check Windows privacy settings.");
		return false;
	}
	if (!Capture->Start())
	{
		OutFailureReason =
			TEXT("Windows did not allow HyperTwist to start the selected microphone.");
		return false;
	}

	Capture->Stop();
	return true;
}

FHyperTwistDesktopAccountState
UHyperTwistPlayerSettingsLibrary::LoadDesktopAccountState()
{
	using namespace HyperTwistPlayerSettingsInternal;
	FHyperTwistDesktopAccountState State;
	if (GConfig == nullptr)
	{
		return State;
	}
	GConfig->GetBool(AccountSection, TEXT("Linked"), State.bLinked, GGameUserSettingsIni);
	GConfig->GetString(AccountSection, TEXT("Email"), State.Email, GGameUserSettingsIni);
	GConfig->GetString(AccountSection, TEXT("Plan"), State.Plan, GGameUserSettingsIni);
	GConfig->GetString(AccountSection, TEXT("Role"), State.Role, GGameUserSettingsIni);
	GConfig->GetBool(AccountSection, TEXT("CanDownload"), State.bCanDownload, GGameUserSettingsIni);
	GConfig->GetString(AccountSection, TEXT("AccessStatus"), State.AccessStatus, GGameUserSettingsIni);
	GConfig->GetString(AccountSection, TEXT("LinkedAtUtc"), State.LinkedAtUtc, GGameUserSettingsIni);
	if (!State.IsStructurallyValid())
	{
		return FHyperTwistDesktopAccountState();
	}
	return State;
}

bool UHyperTwistPlayerSettingsLibrary::SaveDesktopAccountState(
	const FHyperTwistDesktopAccountState& AccountState
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	if (GConfig == nullptr || !AccountState.IsStructurallyValid())
	{
		return false;
	}
	GConfig->SetBool(AccountSection, TEXT("Linked"), AccountState.bLinked, GGameUserSettingsIni);
	GConfig->SetString(AccountSection, TEXT("Email"), *AccountState.Email, GGameUserSettingsIni);
	GConfig->SetString(AccountSection, TEXT("Plan"), *AccountState.Plan, GGameUserSettingsIni);
	GConfig->SetString(AccountSection, TEXT("Role"), *AccountState.Role, GGameUserSettingsIni);
	GConfig->SetBool(AccountSection, TEXT("CanDownload"), AccountState.bCanDownload, GGameUserSettingsIni);
	GConfig->SetString(AccountSection, TEXT("AccessStatus"), *AccountState.AccessStatus, GGameUserSettingsIni);
	GConfig->SetString(AccountSection, TEXT("LinkedAtUtc"), *AccountState.LinkedAtUtc, GGameUserSettingsIni);
	GConfig->Flush(false, GGameUserSettingsIni);
	return true;
}

void UHyperTwistPlayerSettingsLibrary::ClearDesktopAccountState()
{
	if (GConfig != nullptr)
	{
		GConfig->EmptySection(
			HyperTwistPlayerSettingsInternal::AccountSection,
			GGameUserSettingsIni);
		GConfig->Flush(false, GGameUserSettingsIni);
	}
}

bool UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
	const FString& CredentialSlot,
	const FString& Secret,
	FString& OutFailureReason
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	OutFailureReason.Reset();
	if (GConfig == nullptr)
	{
		OutFailureReason = TEXT("The local settings store is unavailable.");
		return false;
	}
	if (!IsCredentialSlotAllowed(CredentialSlot))
	{
		OutFailureReason = TEXT("The credential slot is invalid.");
		return false;
	}
	if (Secret.IsEmpty())
	{
		DeleteProviderCredential(CredentialSlot);
		return true;
	}
	if (Secret.Len() > 8192)
	{
		OutFailureReason = TEXT("The credential is unexpectedly large.");
		return false;
	}

#if PLATFORM_WINDOWS
	FString ProtectedBase64;
	if (!ProtectString(Secret, ProtectedBase64))
	{
		OutFailureReason = TEXT("Windows could not protect the credential for this user.");
		return false;
	}
	SetConfigString(CredentialSection, *CredentialSlot, ProtectedBase64);
	GConfig->Flush(false, GGameUserSettingsIni);
	return true;
#else
	OutFailureReason =
		TEXT("Persistent provider credentials are currently supported only on Windows.");
	return false;
#endif
}

bool UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
	const FString& CredentialSlot,
	FString& OutSecret
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	OutSecret.Reset();
	if (!IsCredentialSlotAllowed(CredentialSlot) || GConfig == nullptr)
	{
		return false;
	}
	const FString ProtectedBase64 =
		GetConfigString(CredentialSection, *CredentialSlot, FString());
	if (ProtectedBase64.IsEmpty())
	{
		return false;
	}
#if PLATFORM_WINDOWS
	return UnprotectString(ProtectedBase64, OutSecret);
#else
	return false;
#endif
}

bool UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
	const FString& CredentialSlot
)
{
	FString Secret;
	const bool bLoaded = LoadProviderCredential(CredentialSlot, Secret);
	if (!Secret.IsEmpty())
	{
		Secret.Reset(Secret.Len());
	}
	return bLoaded;
}

void UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(
	const FString& CredentialSlot
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	if (IsCredentialSlotAllowed(CredentialSlot) && GConfig != nullptr)
	{
		GConfig->RemoveKey(CredentialSection, *CredentialSlot, GGameUserSettingsIni);
		GConfig->Flush(false, GGameUserSettingsIni);
	}
}

bool UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
	const FString& Endpoint,
	const bool bAllowCloudProviders,
	FString& OutFailureReason
)
{
	using namespace HyperTwistPlayerSettingsInternal;
	OutFailureReason.Reset();
	const FString SafeEndpoint = Endpoint.TrimStartAndEnd();
	if (SafeEndpoint.IsEmpty())
	{
		OutFailureReason = TEXT("Configure a provider endpoint first.");
		return false;
	}
	if (SafeEndpoint.Len() > 2048)
	{
		OutFailureReason = TEXT("The provider endpoint is unexpectedly long.");
		return false;
	}
	for (const TCHAR Character : SafeEndpoint)
	{
		if (FChar::IsWhitespace(Character) || Character < TEXT(' '))
		{
			OutFailureReason = TEXT("The provider endpoint cannot contain whitespace.");
			return false;
		}
	}

	const int32 SchemeLength = SafeEndpoint.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase)
		? 8
		: SafeEndpoint.StartsWith(TEXT("http://"), ESearchCase::IgnoreCase)
			? 7
			: INDEX_NONE;
	if (SchemeLength == INDEX_NONE || SafeEndpoint.Len() <= SchemeLength)
	{
		OutFailureReason = TEXT("Use an http:// loopback URL or an https:// cloud URL.");
		return false;
	}

	int32 AuthorityEnd = SafeEndpoint.Len();
	const TCHAR AuthorityDelimiters[] = {TEXT('/'), TEXT('?'), TEXT('#')};
	for (const TCHAR Delimiter : AuthorityDelimiters)
	{
		const int32 CandidateEnd = SafeEndpoint.Find(
			FString::Chr(Delimiter),
			ESearchCase::CaseSensitive,
			ESearchDir::FromStart,
			SchemeLength);
		if (CandidateEnd != INDEX_NONE)
		{
			AuthorityEnd = FMath::Min(AuthorityEnd, CandidateEnd);
		}
	}
	const FString Authority = SafeEndpoint.Mid(
		SchemeLength,
		AuthorityEnd - SchemeLength);
	if (Authority.IsEmpty() || Authority.Contains(TEXT("@")))
	{
		OutFailureReason = TEXT("The provider endpoint must contain a host and no embedded credentials.");
		return false;
	}

	if (IsLoopbackHttpEndpoint(SafeEndpoint))
	{
		return true;
	}
	if (!SafeEndpoint.StartsWith(TEXT("https://"), ESearchCase::IgnoreCase))
	{
		OutFailureReason = TEXT("Remote providers require HTTPS.");
		return false;
	}
	if (!bAllowCloudProviders)
	{
		OutFailureReason =
			TEXT("Enable cloud providers before sending puzzle context to a remote endpoint.");
		return false;
	}
	return true;
}

const TCHAR* UHyperTwistPlayerSettingsLibrary::GetMainMenuMapAssetPath()
{
	return HyperTwistPlayerSettingsInternal::MainMenuMapAssetPath;
}

const TCHAR* UHyperTwistPlayerSettingsLibrary::GetMainMenuGameModeClassPath()
{
	return HyperTwistPlayerSettingsInternal::MainMenuGameModeClassPath;
}

const TCHAR* UHyperTwistPlayerSettingsLibrary::GetAccountWebsiteUrl()
{
	return HyperTwistPlayerSettingsInternal::AccountWebsiteUrl;
}

const TCHAR* UHyperTwistPlayerSettingsLibrary::GetDesktopLinkVerifyUrl()
{
	return HyperTwistPlayerSettingsInternal::DesktopLinkVerifyUrl;
}
