#include "HyperTwistUX/HyperTwistSettingsPanelWidget.h"

#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/CheckBox.h"
#include "Components/ComboBoxString.h"
#include "Components/EditableTextBox.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/ScrollBox.h"
#include "Components/SizeBox.h"
#include "Components/Slider.h"
#include "Components/Spacer.h"
#include "Components/TextBlock.h"
#include "Components/UniformGridPanel.h"
#include "Components/UniformGridSlot.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/WidgetSwitcher.h"
#include "Input/Reply.h"
#include "InputCoreTypes.h"

namespace HyperTwistSettingsPanelWidgetInternal
{
	constexpr int32 ControlsPageIndex = 0;
	constexpr int32 AudioPageIndex = 1;
	constexpr int32 GraphicsPageIndex = 2;
	constexpr int32 AccessibilityPageIndex = 3;
	constexpr int32 SpeechPageIndex = 4;

	struct FProviderOption
	{
		const TCHAR* Id;
		const TCHAR* Label;
		const TCHAR* DefaultEndpoint;
		const TCHAR* DefaultModel;
	};

	const TArray<FProviderOption>& GetSpeechProviderOptions()
	{
		static const TArray<FProviderOption> Options = {
			{TEXT("local-whisper-cpp"), TEXT("On-device Whisper"), TEXT("http://127.0.0.1:8766"), TEXT("local-whisper/base-q5_1")},
			{TEXT("local-freestyle"), TEXT("Local Freestyle bridge"), TEXT("http://127.0.0.1:4649"), TEXT("local-whisper/base-q5_1")},
			{TEXT("openai"), TEXT("OpenAI via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("openai/gpt-4o-transcribe")},
			{TEXT("groq"), TEXT("Groq via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("groq/whisper-large-v3-turbo")},
			{TEXT("deepgram"), TEXT("Deepgram via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("deepgram/nova-3")},
			{TEXT("elevenlabs"), TEXT("ElevenLabs via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("elevenlabs/scribe_v2_realtime")}
		};
		return Options;
	}

	const TArray<FProviderOption>& GetVoiceProviderOptions()
	{
		static const TArray<FProviderOption> Options = {
			{TEXT("local-piper"), TEXT("On-device Piper"), TEXT("http://127.0.0.1:8766"), TEXT("piper-medium")},
			{TEXT("openai"), TEXT("OpenAI via voice bridge"), TEXT("http://127.0.0.1:8766"), TEXT("gpt-4o-mini-tts")},
			{TEXT("elevenlabs"), TEXT("ElevenLabs via voice bridge"), TEXT("http://127.0.0.1:8766"), TEXT("eleven_multilingual_v2")},
			{TEXT("custom-compatible"), TEXT("Custom compatible service"), TEXT(""), TEXT("")}
		};
		return Options;
	}

	const TArray<FProviderOption>& GetCoachProviderOptions()
	{
		static const TArray<FProviderOption> Options = {
			{TEXT("local-disabled"), TEXT("Built-in guide only"), TEXT(""), TEXT("")},
			{TEXT("local-openai-compatible"), TEXT("Local OpenAI-compatible"), TEXT("http://127.0.0.1:11434/v1"), TEXT("")},
			{TEXT("openai"), TEXT("OpenAI"), TEXT("https://api.openai.com/v1/responses"), TEXT("gpt-5.5")},
			{TEXT("groq"), TEXT("Groq"), TEXT("https://api.groq.com/openai/v1"), TEXT("")},
			{TEXT("custom-openai-compatible"), TEXT("Custom OpenAI-compatible"), TEXT(""), TEXT("")}
		};
		return Options;
	}

	const FProviderOption* FindProviderById(
		const TArray<FProviderOption>& Options,
		const FString& Id
	)
	{
		return Options.FindByPredicate(
			[&Id](const FProviderOption& Option)
			{
				return Id.Equals(Option.Id, ESearchCase::IgnoreCase);
			});
	}

	const FProviderOption* FindProviderByLabel(
		const TArray<FProviderOption>& Options,
		const FString& Label
	)
	{
		return Options.FindByPredicate(
			[&Label](const FProviderOption& Option)
			{
				return Label.Equals(Option.Label, ESearchCase::IgnoreCase);
			});
	}

	TArray<FString> BuildProviderLabels(const TArray<FProviderOption>& Options)
	{
		TArray<FString> Labels;
		Labels.Reserve(Options.Num());
		for (const FProviderOption& Option : Options)
		{
			Labels.Add(Option.Label);
		}
		return Labels;
	}

	FString ProviderLabel(
		const TArray<FProviderOption>& Options,
		const FString& Id
	)
	{
		if (const FProviderOption* Option = FindProviderById(Options, Id))
		{
			return Option->Label;
		}
		return Options.Num() > 0 ? FString(Options[0].Label) : FString();
	}

	FString SpeechLanguageLabel(const FString& LanguageCode)
	{
		if (LanguageCode.Equals(TEXT("en"), ESearchCase::IgnoreCase)) return TEXT("English");
		if (LanguageCode.Equals(TEXT("de"), ESearchCase::IgnoreCase)) return TEXT("German");
		if (LanguageCode.Equals(TEXT("fr"), ESearchCase::IgnoreCase)) return TEXT("French");
		if (LanguageCode.Equals(TEXT("es"), ESearchCase::IgnoreCase)) return TEXT("Spanish");
		if (LanguageCode.Equals(TEXT("it"), ESearchCase::IgnoreCase)) return TEXT("Italian");
		if (LanguageCode.Equals(TEXT("pt"), ESearchCase::IgnoreCase)) return TEXT("Portuguese");
		if (LanguageCode.Equals(TEXT("ja"), ESearchCase::IgnoreCase)) return TEXT("Japanese");
		if (LanguageCode.Equals(TEXT("ko"), ESearchCase::IgnoreCase)) return TEXT("Korean");
		if (LanguageCode.Equals(TEXT("zh"), ESearchCase::IgnoreCase)) return TEXT("Chinese");
		return TEXT("Auto-detect");
	}

	FString SpeechLanguageCode(const FString& LanguageLabel)
	{
		if (LanguageLabel == TEXT("English")) return TEXT("en");
		if (LanguageLabel == TEXT("German")) return TEXT("de");
		if (LanguageLabel == TEXT("French")) return TEXT("fr");
		if (LanguageLabel == TEXT("Spanish")) return TEXT("es");
		if (LanguageLabel == TEXT("Italian")) return TEXT("it");
		if (LanguageLabel == TEXT("Portuguese")) return TEXT("pt");
		if (LanguageLabel == TEXT("Japanese")) return TEXT("ja");
		if (LanguageLabel == TEXT("Korean")) return TEXT("ko");
		if (LanguageLabel == TEXT("Chinese")) return TEXT("zh");
		return TEXT("auto");
	}

	FString PercentLabel(const float Value)
	{
		return FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Value * 100.0f));
	}

	FString DecimalLabel(const float Value)
	{
		return FString::Printf(TEXT("%.2fx"), Value);
	}

	void SetText(UTextBlock* TextBlock, const FString& Text)
	{
		if (TextBlock != nullptr)
		{
			TextBlock->SetText(FText::FromString(Text));
		}
	}

	FString QualityLabel(const EHyperTwistGraphicsQuality Quality)
	{
		switch (Quality)
		{
		case EHyperTwistGraphicsQuality::Low:
			return TEXT("Low");
		case EHyperTwistGraphicsQuality::Medium:
			return TEXT("Medium");
		case EHyperTwistGraphicsQuality::Epic:
			return TEXT("Epic");
		default:
			return TEXT("High");
		}
	}

	FString WindowModeLabel(const EHyperTwistWindowMode WindowMode)
	{
		switch (WindowMode)
		{
		case EHyperTwistWindowMode::Fullscreen:
			return TEXT("Fullscreen");
		case EHyperTwistWindowMode::Windowed:
			return TEXT("Windowed");
		default:
			return TEXT("Borderless");
		}
	}
}

void UHyperTwistKeyBindingRowWidget::ConfigureBinding(
	const FName InActionId,
	const FString& InLabel,
	const FString& InCategory,
	const FKey InKey
)
{
	ActionId = InActionId;
	DisplayLabel = InLabel;
	CategoryLabel = InCategory;
	BoundKey = InKey;
	EnsureWidgetTreeBuilt();
	if (BindingLabel != nullptr)
	{
		BindingLabel->SetText(FText::FromString(DisplayLabel));
	}
	UpdateBoundKey(BoundKey);
}

void UHyperTwistKeyBindingRowWidget::UpdateBoundKey(
	const FKey InKey,
	const bool bAwaitingInput
)
{
	BoundKey = InKey;
	if (KeyLabel != nullptr)
	{
		KeyLabel->SetText(FText::FromString(
			bAwaitingInput ? TEXT("Press a key...") : BoundKey.GetDisplayName().ToString()));
	}
}

TSharedRef<SWidget> UHyperTwistKeyBindingRowWidget::RebuildWidget()
{
	Initialize();
	EnsureWidgetTreeBuilt();
	return Super::RebuildWidget();
}

void UHyperTwistKeyBindingRowWidget::EnsureWidgetTreeBuilt()
{
	if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
	{
		return;
	}

	UHorizontalBox* Row = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("KeyBindingRow"));
	WidgetTree->RootWidget = Row;

	USizeBox* LabelSize = WidgetTree->ConstructWidget<USizeBox>(
		USizeBox::StaticClass(),
		TEXT("KeyBindingLabelSize"));
	LabelSize->SetWidthOverride(360.0f);
	BindingLabel = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass(),
		TEXT("KeyBindingLabel"));
	BindingLabel->SetText(FText::FromString(DisplayLabel));
	BindingLabel->SetColorAndOpacity(FSlateColor(FLinearColor(0.88f, 0.94f, 0.98f, 1.0f)));
	FSlateFontInfo BindingFont = BindingLabel->GetFont();
	BindingFont.Size = 14;
	BindingLabel->SetFont(BindingFont);
	LabelSize->AddChild(BindingLabel);
	if (UHorizontalBoxSlot* LabelSlot = Row->AddChildToHorizontalBox(LabelSize))
	{
		LabelSlot->SetPadding(FMargin(0.0f, 6.0f, 12.0f, 6.0f));
		LabelSlot->SetVerticalAlignment(VAlign_Center);
	}

	UButton* RebindButton = WidgetTree->ConstructWidget<UButton>(
		UButton::StaticClass(),
		TEXT("KeyBindingButton"));
	RebindButton->SetBackgroundColor(FLinearColor(0.07f, 0.15f, 0.20f, 1.0f));
	KeyLabel = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass(),
		TEXT("KeyBindingValue"));
	KeyLabel->SetText(FText::FromString(BoundKey.GetDisplayName().ToString()));
	KeyLabel->SetJustification(ETextJustify::Center);
	KeyLabel->SetColorAndOpacity(FSlateColor(FLinearColor(0.12f, 0.95f, 0.82f, 1.0f)));
	FSlateFontInfo KeyFont = KeyLabel->GetFont();
	KeyFont.Size = 14;
	KeyLabel->SetFont(KeyFont);
	RebindButton->AddChild(KeyLabel);
	if (UButtonSlot* KeyButtonSlot = Cast<UButtonSlot>(KeyLabel->Slot))
	{
		KeyButtonSlot->SetPadding(FMargin(18.0f, 7.0f));
		KeyButtonSlot->SetHorizontalAlignment(HAlign_Center);
	}
	RebindButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistKeyBindingRowWidget::HandleRebindClicked);
	if (UHorizontalBoxSlot* ButtonSlot = Row->AddChildToHorizontalBox(RebindButton))
	{
		ButtonSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
		ButtonSlot->SetPadding(FMargin(0.0f, 4.0f));
		ButtonSlot->SetVerticalAlignment(VAlign_Center);
	}
}

void UHyperTwistKeyBindingRowWidget::HandleRebindClicked()
{
	UpdateBoundKey(BoundKey, true);
	OnRebindRequested.Broadcast(ActionId);
}

UHyperTwistSettingsPanelWidget::UHyperTwistSettingsPanelWidget(
	const FObjectInitializer& ObjectInitializer
)
	: Super(ObjectInitializer)
{
	SetIsFocusable(true);
}

void UHyperTwistSettingsPanelWidget::NativeConstruct()
{
	Super::NativeConstruct();
	EnsureWidgetTreeBuilt();
	ReloadPreferences();
}

TSharedRef<SWidget> UHyperTwistSettingsPanelWidget::RebuildWidget()
{
	Initialize();
	EnsureWidgetTreeBuilt();
	return Super::RebuildWidget();
}

bool UHyperTwistSettingsPanelWidget::PrepareSettingsSurface()
{
	EnsureWidgetTreeBuilt();
	ReloadPreferences();
	return IsSettingsSurfaceReady();
}

bool UHyperTwistSettingsPanelWidget::IsSettingsSurfaceReady() const
{
	return WidgetTree != nullptr
		&& WidgetTree->RootWidget != nullptr
		&& SettingsPageSwitcher != nullptr
		&& SettingsPageSwitcher->GetNumWidgets() == 5
		&& KeyboardProfileCombo != nullptr
		&& MasterVolumeSlider != nullptr;
}

void UHyperTwistSettingsPanelWidget::ReloadPreferences()
{
	Preferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	RefreshWidgetsFromPreferences();
	SetStatus(TEXT("Settings are saved automatically on this device."));
}

FReply UHyperTwistSettingsPanelWidget::NativeOnKeyDown(
	const FGeometry& InGeometry,
	const FKeyEvent& InKeyEvent
)
{
	static_cast<void>(InGeometry);
	const FKey PressedKey = InKeyEvent.GetKey();
	if (TryCompletePendingRebind(PressedKey))
	{
		return FReply::Handled();
	}

	if (PressedKey == EKeys::Escape && bShowCloseButton)
	{
		OnCloseRequested.Broadcast();
		return FReply::Handled();
	}
	return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}

FReply UHyperTwistSettingsPanelWidget::NativeOnPreviewMouseButtonDown(
	const FGeometry& InGeometry,
	const FPointerEvent& InMouseEvent
)
{
	if (TryCompletePendingRebind(InMouseEvent.GetEffectingButton()))
	{
		return FReply::Handled();
	}
	return Super::NativeOnPreviewMouseButtonDown(InGeometry, InMouseEvent);
}

bool UHyperTwistSettingsPanelWidget::TryCompletePendingRebind(
	const FKey PressedKey
)
{
	if (PendingRebindActionId.IsNone())
	{
		return false;
	}
	if (PressedKey == EKeys::Escape)
	{
		PendingRebindActionId = NAME_None;
		RefreshBindingRows();
		SetStatus(TEXT("Key change cancelled."));
		return true;
	}

	FString FailureReason;
	if (!UHyperTwistPlayerSettingsLibrary::TrySetKeyBinding(
			Preferences,
			PendingRebindActionId,
			PressedKey,
			FailureReason))
	{
		SetStatus(FailureReason, true);
		return true;
	}

	PendingRebindActionId = NAME_None;
	RefreshBindingRows();
	SavePreferencesAndApply(TEXT("Control binding updated."));
	if (KeyboardProfileCombo != nullptr)
	{
		KeyboardProfileCombo->SetSelectedOption(TEXT("Custom"));
	}
	return true;
}

void UHyperTwistSettingsPanelWidget::EnsureWidgetTreeBuilt()
{
	if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
	{
		return;
	}

	Preferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();

	UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(
		UBorder::StaticClass(),
		TEXT("HyperTwistSettingsRoot"));
	RootBorder->SetPadding(FMargin(28.0f));
	RootBorder->SetBrushColor(PanelColor);
	WidgetTree->RootWidget = RootBorder;

	UVerticalBox* Root = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass(),
		TEXT("HyperTwistSettingsLayout"));
	RootBorder->AddChild(Root);

	UHorizontalBox* Header = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("HyperTwistSettingsHeader"));
	if (UVerticalBoxSlot* HeaderSlot = Root->AddChildToVerticalBox(Header))
	{
		HeaderSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 18.0f));
	}

	UVerticalBox* HeaderCopy = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass(),
		TEXT("HyperTwistSettingsHeaderCopy"));
	if (UHorizontalBoxSlot* CopySlot = Header->AddChildToHorizontalBox(HeaderCopy))
	{
		CopySlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}
	AddText(HeaderCopy, TEXT("SETTINGS"), 13, AccentColor);
	AddText(HeaderCopy, TEXT("Shape HyperTwist around you"), 29, PrimaryTextColor);
	AddText(
		HeaderCopy,
		TEXT("Every desktop preference is local-first, reversible, and available without a headset."),
		14,
		MutedTextColor);

	if (bShowCloseButton)
	{
		UButton* CloseButton = AddActionButton(
			Header,
			TEXT("Close"),
			TEXT("HyperTwistSettingsCloseButton"));
		CloseButton->OnClicked.AddDynamic(
			this,
			&UHyperTwistSettingsPanelWidget::HandleCloseClicked);
	}

	UUniformGridPanel* Tabs = WidgetTree->ConstructWidget<UUniformGridPanel>(
		UUniformGridPanel::StaticClass(),
		TEXT("HyperTwistSettingsTabs"));
	Tabs->SetMinDesiredSlotWidth(150.0f);
	Tabs->SetSlotPadding(FMargin(3.0f));
	if (UVerticalBoxSlot* TabSlot = Root->AddChildToVerticalBox(Tabs))
	{
		TabSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 16.0f));
	}

	UButton* ControlsTab = AddTabButton(Tabs, TEXT("Controls"), 0, TEXT("SettingsControlsTab"));
	UButton* AudioTab = AddTabButton(Tabs, TEXT("Audio"), 1, TEXT("SettingsAudioTab"));
	UButton* GraphicsTab = AddTabButton(Tabs, TEXT("Graphics"), 2, TEXT("SettingsGraphicsTab"));
	UButton* AccessibilityTab = AddTabButton(
		Tabs,
		TEXT("Accessibility"),
		3,
		TEXT("SettingsAccessibilityTab"));
	UButton* SpeechTab = AddTabButton(
		Tabs,
		TEXT("Speech & Coach"),
		4,
		TEXT("SettingsSpeechTab"));
	ControlsTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowControlsPage);
	AudioTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowAudioPage);
	GraphicsTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowGraphicsPage);
	AccessibilityTab->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::ShowAccessibilityPage);
	SpeechTab->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::ShowSpeechAndCoachPage);

	SettingsPageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(
		UWidgetSwitcher::StaticClass(),
		TEXT("HyperTwistSettingsPageSwitcher"));
	if (UVerticalBoxSlot* SwitcherSlot = Root->AddChildToVerticalBox(SettingsPageSwitcher))
	{
		SwitcherSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}

	BuildControlsPage(AddScrollablePage(TEXT("SettingsControlsPage")));
	BuildAudioPage(AddScrollablePage(TEXT("SettingsAudioPage")));
	BuildGraphicsPage(AddScrollablePage(TEXT("SettingsGraphicsPage")));
	BuildAccessibilityPage(AddScrollablePage(TEXT("SettingsAccessibilityPage")));
	BuildSpeechAndCoachPage(AddScrollablePage(TEXT("SettingsSpeechPage")));
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::ControlsPageIndex);

	StatusText = AddText(
		Root,
		TEXT("Settings are saved automatically on this device."),
		13,
		MutedTextColor,
		TEXT("HyperTwistSettingsStatus"));
	if (UVerticalBoxSlot* StatusSlot = Cast<UVerticalBoxSlot>(
			StatusText != nullptr ? StatusText->Slot : nullptr))
	{
		StatusSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 0.0f));
	}

	RefreshWidgetsFromPreferences();
}

void UHyperTwistSettingsPanelWidget::BuildControlsPage(UVerticalBox* Parent)
{
	AddSectionIntro(
		Parent,
		TEXT("INPUT"),
		TEXT("Fast when you want it, forgiving when you need it"),
		TEXT("Choose the established WCA speedcubing layout, a mnemonic pair layout, or bind every action yourself. Clicking a key starts capture; Escape cancels."));

	UHorizontalBox* ProfileRow = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("KeyboardProfileRow"));
	Parent->AddChildToVerticalBox(ProfileRow);
	UTextBlock* ProfileLabel = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass(),
		TEXT("KeyboardProfileLabel"));
	ProfileLabel->SetText(FText::FromString(TEXT("Keyboard layout")));
	ProfileLabel->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
	FSlateFontInfo LabelFont = ProfileLabel->GetFont();
	LabelFont.Size = 15;
	ProfileLabel->SetFont(LabelFont);
	if (UHorizontalBoxSlot* LabelSlot = ProfileRow->AddChildToHorizontalBox(ProfileLabel))
	{
		LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
		LabelSlot->SetVerticalAlignment(VAlign_Center);
	}
	KeyboardProfileCombo = WidgetTree->ConstructWidget<UComboBoxString>(
		UComboBoxString::StaticClass(),
		TEXT("KeyboardProfileCombo"));
	KeyboardProfileCombo->AddOption(TEXT("WCA Pro"));
	KeyboardProfileCombo->AddOption(TEXT("Mnemonic Pairs"));
	KeyboardProfileCombo->AddOption(TEXT("Custom"));
	KeyboardProfileCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleKeyboardProfileChanged);
	if (UHorizontalBoxSlot* ComboSlot = ProfileRow->AddChildToHorizontalBox(KeyboardProfileCombo))
	{
		ComboSlot->SetPadding(FMargin(18.0f, 6.0f));
		ComboSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}

	PointerSensitivitySlider = AddSliderRow(
		Parent,
		TEXT("Pointer sensitivity"),
		TEXT("Controls picking and direct manipulation."),
		TEXT("PointerSensitivitySlider"),
		PointerSensitivityValue);
	OrbitSensitivitySlider = AddSliderRow(
		Parent,
		TEXT("Orbit sensitivity"),
		TEXT("Controls middle-mouse and Shift + right-mouse camera movement."),
		TEXT("OrbitSensitivitySlider"),
		OrbitSensitivityValue);
	ZoomSensitivitySlider = AddSliderRow(
		Parent,
		TEXT("Zoom sensitivity"),
		TEXT("Controls wheel and trackpad zoom speed."),
		TEXT("ZoomSensitivitySlider"),
		ZoomSensitivityValue);
	PointerSensitivitySlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandlePointerSensitivityChanged);
	OrbitSensitivitySlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleOrbitSensitivityChanged);
	ZoomSensitivitySlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleZoomSensitivityChanged);

	InvertOrbitCheck = AddCheckRow(
		Parent,
		TEXT("Invert vertical orbit"),
		TEXT("Reverses vertical camera drag without changing puzzle turns."),
		TEXT("InvertOrbitCheck"));
	TouchInputCheck = AddCheckRow(
		Parent,
		TEXT("Enable touch input"),
		TEXT("Keeps direct touch turns available on compatible displays."),
		TEXT("TouchInputCheck"));
	InvertOrbitCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleInvertOrbitChanged);
	TouchInputCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleTouchInputChanged);

	AddText(Parent, TEXT("KEY BINDINGS"), 13, WarmAccentColor);
	AddText(
		Parent,
		TEXT("Conflicts are resolved by swapping the two keys, so no action silently disappears."),
		13,
		MutedTextColor);

	BindingRows.Reset();
	for (const FHyperTwistKeyBindingDescriptor& Descriptor :
			UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(Preferences))
	{
		UHyperTwistKeyBindingRowWidget* Row =
			WidgetTree->ConstructWidget<UHyperTwistKeyBindingRowWidget>(
				UHyperTwistKeyBindingRowWidget::StaticClass());
		Row->ConfigureBinding(
			Descriptor.ActionId,
			Descriptor.Label,
			Descriptor.Category,
			Descriptor.Key);
		Row->OnRebindRequested.AddDynamic(
			this,
			&UHyperTwistSettingsPanelWidget::HandleBindingRequested);
		if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
		{
			RowSlot->SetPadding(FMargin(0.0f, 2.0f));
		}
		BindingRows.Add(Row);
	}

	UHorizontalBox* ResetRow = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("SettingsResetRow"));
	if (UVerticalBoxSlot* ResetRowSlot = Parent->AddChildToVerticalBox(ResetRow))
	{
		ResetRowSlot->SetPadding(FMargin(0.0f, 16.0f, 0.0f, 8.0f));
	}
	UButton* ResetButton = AddActionButton(
		ResetRow,
		TEXT("Restore all defaults"),
		TEXT("SettingsResetDefaultsButton"));
	ResetButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleResetDefaultsClicked);
}

void UHyperTwistSettingsPanelWidget::BuildAudioPage(UVerticalBox* Parent)
{
	AddSectionIntro(
		Parent,
		TEXT("SOUND"),
		TEXT("Hear the turn, keep the focus"),
		TEXT("Balance puzzle feedback, music, spoken teaching, and overall output. Muting never changes saved channel levels."));
	MasterVolumeSlider = AddSliderRow(
		Parent,
		TEXT("Master volume"),
		TEXT("Overall HyperTwist output."),
		TEXT("MasterVolumeSlider"),
		MasterVolumeValue);
	MusicVolumeSlider = AddSliderRow(
		Parent,
		TEXT("Music"),
		TEXT("Ambient and menu music."),
		TEXT("MusicVolumeSlider"),
		MusicVolumeValue);
	EffectsVolumeSlider = AddSliderRow(
		Parent,
		TEXT("Puzzle effects"),
		TEXT("Turns, confirmations, timers, and interface feedback."),
		TEXT("EffectsVolumeSlider"),
		EffectsVolumeValue);
	VoiceVolumeSlider = AddSliderRow(
		Parent,
		TEXT("Coach voice"),
		TEXT("Narration and spoken guidance."),
		TEXT("VoiceVolumeSlider"),
		VoiceVolumeValue);
	MasterVolumeSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleMasterVolumeChanged);
	MusicVolumeSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleMusicVolumeChanged);
	EffectsVolumeSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleEffectsVolumeChanged);
	VoiceVolumeSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceVolumeChanged);
	MutedCheck = AddCheckRow(
		Parent,
		TEXT("Mute all sound"),
		TEXT("Temporarily silences HyperTwist while preserving the mix above."),
		TEXT("MutedCheck"));
	MutedCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleMutedChanged);
}

void UHyperTwistSettingsPanelWidget::BuildGraphicsPage(UVerticalBox* Parent)
{
	AddSectionIntro(
		Parent,
		TEXT("DISPLAY"),
		TEXT("Clarity before spectacle"),
		TEXT("Tune visual quality for the puzzle order and hardware you are using. High is the balanced default; larger 4D slices may benefit from Medium."));

	GraphicsQualityCombo = WidgetTree->ConstructWidget<UComboBoxString>(
		UComboBoxString::StaticClass(),
		TEXT("GraphicsQualityCombo"));
	const TArray<FString> QualityOptions = {
		TEXT("Low"),
		TEXT("Medium"),
		TEXT("High"),
		TEXT("Epic")
	};
	for (const FString& Option : QualityOptions)
	{
		GraphicsQualityCombo->AddOption(Option);
	}
	GraphicsQualityCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleGraphicsQualityChanged);
	AddText(Parent, TEXT("Overall quality"), 15, PrimaryTextColor);
	Parent->AddChildToVerticalBox(GraphicsQualityCombo);

	WindowModeCombo = WidgetTree->ConstructWidget<UComboBoxString>(
		UComboBoxString::StaticClass(),
		TEXT("WindowModeCombo"));
	const TArray<FString> WindowModeOptions = {
		TEXT("Fullscreen"),
		TEXT("Borderless"),
		TEXT("Windowed")
	};
	for (const FString& Option : WindowModeOptions)
	{
		WindowModeCombo->AddOption(Option);
	}
	WindowModeCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleWindowModeChanged);
	AddText(Parent, TEXT("Display mode"), 15, PrimaryTextColor);
	Parent->AddChildToVerticalBox(WindowModeCombo);

	ResolutionScaleSlider = AddSliderRow(
		Parent,
		TEXT("Render scale"),
		TEXT("Lower this to regain performance without shrinking the interface."),
		TEXT("ResolutionScaleSlider"),
		ResolutionScaleValue);
	ResolutionScaleSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleResolutionScaleChanged);
	VSyncCheck = AddCheckRow(
		Parent,
		TEXT("Vertical sync"),
		TEXT("Reduces tearing by synchronizing frames with the display."),
		TEXT("VSyncCheck"));
	VSyncCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVSyncChanged);
}

void UHyperTwistSettingsPanelWidget::BuildAccessibilityPage(UVerticalBox* Parent)
{
	AddSectionIntro(
		Parent,
		TEXT("ACCESS"),
		TEXT("A readable path through every dimension"),
		TEXT("Scale interface text and reduce non-essential motion without changing puzzle state or timing."));
	UiScaleSlider = AddSliderRow(
		Parent,
		TEXT("Interface scale"),
		TEXT("Scales menus, labels, and teaching surfaces from 80% to 140%."),
		TEXT("UiScaleSlider"),
		UiScaleValue);
	UiScaleSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleUiScaleChanged);
	ReducedMotionCheck = AddCheckRow(
		Parent,
		TEXT("Reduce motion"),
		TEXT("Shortens decorative transitions while preserving puzzle animation cues."),
		TEXT("ReducedMotionCheck"));
	HighContrastCheck = AddCheckRow(
		Parent,
		TEXT("High contrast"),
		TEXT("Strengthens separation between panels, labels, and state colors."),
		TEXT("HighContrastCheck"));
	SubtitlesCheck = AddCheckRow(
		Parent,
		TEXT("Subtitles"),
		TEXT("Shows text alongside spoken coaching and voice feedback."),
		TEXT("SubtitlesCheck"));
	ReducedMotionCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleReducedMotionChanged);
	HighContrastCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleHighContrastChanged);
	SubtitlesCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSubtitlesChanged);
}

void UHyperTwistSettingsPanelWidget::BuildSpeechAndCoachPage(UVerticalBox* Parent)
{
	AddSectionIntro(
		Parent,
		TEXT("VOICE & AI"),
		TEXT("Optional intelligence, always under your control"),
		TEXT("HyperTwist works without speech or cloud services. Local processing stays the default; remote providers require explicit consent, and secrets are protected for the current Windows account."));

	SpeechInputCheck = AddCheckRow(
		Parent,
		TEXT("Enable speech-to-text"),
		TEXT("Registers the global dictation shortcut. Microphone capture occurs only while explicitly active."),
		TEXT("SpeechInputCheck"));
	CoachNarrationCheck = AddCheckRow(
		Parent,
		TEXT("Coach narration"),
		TEXT("Lets teaching surfaces speak when a configured voice provider is available."),
		TEXT("CoachNarrationCheck"));
	AssistantPanelEnabledCheck = AddCheckRow(
		Parent,
		TEXT("Fixed in-game coach"),
		TEXT("Makes the native assistant panel available in every puzzle. Use its configured shortcut to open or close it."),
		TEXT("AssistantPanelEnabledCheck"));
	AssistantPanelOpenByDefaultCheck = AddCheckRow(
		Parent,
		TEXT("Open coach with each puzzle"),
		TEXT("Shows the fixed assistant panel when a playable runtime begins."),
		TEXT("AssistantPanelOpenByDefaultCheck"));
	CoachAiEnabledCheck = AddCheckRow(
		Parent,
		TEXT("Enable AI responses"),
		TEXT("When off, the panel remains useful as a private built-in controls and puzzle guide."),
		TEXT("CoachAiEnabledCheck"));
	CloudProvidersCheck = AddCheckRow(
		Parent,
		TEXT("Allow cloud providers"),
		TEXT("Required before HyperTwist sends text, speech, or puzzle context to any non-loopback endpoint."),
		TEXT("CloudProvidersCheck"));
	SpeechInputCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechInputChanged);
	CoachNarrationCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachNarrationChanged);
	CloudProvidersCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCloudProvidersChanged);
	AssistantPanelEnabledCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleAssistantPanelEnabledChanged);
	AssistantPanelOpenByDefaultCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleAssistantPanelOpenByDefaultChanged);
	CoachAiEnabledCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachAiEnabledChanged);

	AddText(Parent, TEXT("SPEECH TO TEXT"), 13, WarmAccentColor);
	SpeechReadinessText = AddText(
		Parent,
		TEXT("STATUS  |  Checking local speech setup..."),
		11,
		MutedTextColor,
		TEXT("SpeechReadinessText"));
	SpeechProviderCombo = AddComboSetting(
		Parent,
		TEXT("Transcription provider"),
		TEXT("Choose an on-device engine, local bridge, or explicit cloud service."),
		TEXT("SpeechProviderCombo"),
		HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
			HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions()));
	SpeechActivationModeCombo = AddComboSetting(
		Parent,
		TEXT("Shortcut behavior"),
		TEXT("Hold records while pressed; Toggle starts and stops on separate presses."),
			TEXT("SpeechActivationModeCombo"),
			{TEXT("Hold to talk"), TEXT("Toggle recording")});
	SpeechDictationDestinationCombo = AddComboSetting(
		Parent,
		TEXT("Dictation destination"),
		TEXT("Draft lets you review the transcript; immediate send asks the fixed coach as soon as transcription completes."),
		TEXT("SpeechDictationDestinationCombo"),
		{TEXT("Coach draft"), TEXT("Ask coach immediately")});
	SpeechLanguageCombo = AddComboSetting(
		Parent,
		TEXT("Language"),
		TEXT("Auto-detect, or bias recognition toward a selected language."),
		TEXT("SpeechLanguageCombo"),
		{
			TEXT("Auto-detect"),
			TEXT("English"),
			TEXT("German"),
			TEXT("French"),
			TEXT("Spanish"),
			TEXT("Italian"),
			TEXT("Portuguese"),
			TEXT("Japanese"),
			TEXT("Korean"),
			TEXT("Chinese")
		});
	SpeechModelInput = AddTextSetting(
		Parent,
		TEXT("Transcription model"),
		TEXT("Provider-qualified model identifier or local model profile."),
		TEXT("SpeechModelInput"));
	AvailableMicrophones =
		UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones();
	TArray<FString> MicrophoneLabels;
	for (const FHyperTwistMicrophoneDescriptor& Microphone : AvailableMicrophones)
	{
		MicrophoneLabels.AddUnique(Microphone.DisplayName);
	}
	SpeechMicrophoneCombo = AddComboSetting(
		Parent,
		TEXT("Microphone"),
		TEXT("Choose a detected Windows input device. System default follows the current operating-system selection."),
		TEXT("SpeechMicrophoneCombo"),
		MicrophoneLabels);
	SpeechContextInput = AddTextSetting(
		Parent,
		TEXT("Recognition context"),
		TEXT("Puzzle names and specialist terms that should bias transcription accuracy."),
		TEXT("SpeechContextInput"));
	SpeechEndpointInput = AddTextSetting(
		Parent,
		TEXT("Endpoint"),
		TEXT("Loopback HTTP is allowed. Remote services must use HTTPS and cloud consent."),
		TEXT("SpeechEndpointInput"));
	SpeechCleanupCheck = AddCheckRow(
		Parent,
		TEXT("Clean up dictated text"),
		TEXT("Applies punctuation and configured vocabulary normalization."),
		TEXT("SpeechCleanupCheck"));
	SpeechSoundFeedbackCheck = AddCheckRow(
		Parent,
		TEXT("Recording sound feedback"),
		TEXT("Plays a short cue when dictation starts and stops."),
		TEXT("SpeechSoundFeedbackCheck"));
	SpeechCredentialInput = AddTextSetting(
		Parent,
		TEXT("API key"),
		TEXT("Leave blank to keep the currently protected key."),
		TEXT("SpeechCredentialInput"),
		true);
	UHorizontalBox* SpeechCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("SpeechCredentialButtonRow"));
	Parent->AddChildToVerticalBox(SpeechCredentialRow);
	UButton* SpeechSaveButton = AddActionButton(
		SpeechCredentialRow,
		TEXT("Save protected key"),
		TEXT("SpeechCredentialSaveButton"));
	UButton* SpeechRemoveButton = AddActionButton(
		SpeechCredentialRow,
		TEXT("Remove key"),
		TEXT("SpeechCredentialRemoveButton"));
	UButton* SpeechValidateButton = AddActionButton(
		SpeechCredentialRow,
		TEXT("Check configuration"),
		TEXT("SpeechConfigurationValidateButton"));
	UButton* SpeechRefreshMicrophonesButton = AddActionButton(
		SpeechCredentialRow,
		TEXT("Refresh microphones"),
		TEXT("SpeechRefreshMicrophonesButton"));
	UButton* SpeechTestMicrophoneButton = AddActionButton(
		SpeechCredentialRow,
		TEXT("Test microphone"),
		TEXT("SpeechTestMicrophoneButton"));
	SpeechSaveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSaveSpeechCredential);
	SpeechRemoveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleRemoveSpeechCredential);
	SpeechValidateButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleValidateSpeechConfiguration);
	SpeechRefreshMicrophonesButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleRefreshMicrophones);
	SpeechTestMicrophoneButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleTestMicrophone);

	AddText(Parent, TEXT("TEXT TO SPEECH"), 13, WarmAccentColor);
	VoiceReadinessText = AddText(
		Parent,
		TEXT("STATUS  |  Checking voice setup..."),
		11,
		MutedTextColor,
		TEXT("VoiceReadinessText"));
	VoiceProviderCombo = AddComboSetting(
		Parent,
		TEXT("Voice provider"),
		TEXT("On-device Piper is the private default; cloud voices remain optional."),
		TEXT("VoiceProviderCombo"),
		HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
			HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions()));
	VoiceModelInput = AddTextSetting(
		Parent,
		TEXT("Voice model"),
		TEXT("Model identifier used by the selected voice service."),
		TEXT("VoiceModelInput"));
	VoiceIdInput = AddTextSetting(
		Parent,
		TEXT("Voice"),
		TEXT("Provider voice identifier, for example en_US-lessac-medium or alloy."),
		TEXT("VoiceIdInput"));
	VoiceSpeedSlider = AddSliderRow(
		Parent,
		TEXT("Speaking speed"),
		TEXT("Adjust narration from 0.5x to 2.0x without changing subtitles."),
		TEXT("VoiceSpeedSlider"),
		VoiceSpeedValue);
	VoiceEndpointInput = AddTextSetting(
		Parent,
		TEXT("Endpoint"),
		TEXT("Loopback HTTP is allowed. Remote services must use HTTPS and cloud consent."),
		TEXT("VoiceEndpointInput"));
	VoiceCredentialInput = AddTextSetting(
		Parent,
		TEXT("API key"),
		TEXT("Leave blank to keep the currently protected key."),
		TEXT("VoiceCredentialInput"),
		true);
	UHorizontalBox* VoiceCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("VoiceCredentialButtonRow"));
	Parent->AddChildToVerticalBox(VoiceCredentialRow);
	UButton* VoiceSaveButton = AddActionButton(
		VoiceCredentialRow,
		TEXT("Save protected key"),
		TEXT("VoiceCredentialSaveButton"));
	UButton* VoiceRemoveButton = AddActionButton(
		VoiceCredentialRow,
		TEXT("Remove key"),
		TEXT("VoiceCredentialRemoveButton"));
	UButton* VoiceValidateButton = AddActionButton(
		VoiceCredentialRow,
		TEXT("Check configuration"),
		TEXT("VoiceConfigurationValidateButton"));
	VoiceSaveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSaveVoiceCredential);
	VoiceRemoveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleRemoveVoiceCredential);
	VoiceValidateButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleValidateVoiceConfiguration);

	AddText(Parent, TEXT("AI COACH"), 13, WarmAccentColor);
	CoachReadinessText = AddText(
		Parent,
		TEXT("STATUS  |  Private built-in guide ready."),
		11,
		MutedTextColor,
		TEXT("CoachReadinessText"));
	CoachProviderCombo = AddComboSetting(
		Parent,
		TEXT("Provider"),
		TEXT("The built-in guide works without AI. Select a model provider only for generated responses."),
		TEXT("CoachProviderCombo"),
		HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
			HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions()));
	CoachEndpointInput = AddTextSetting(
		Parent,
		TEXT("Endpoint"),
		TEXT("OpenAI Responses or OpenAI-compatible chat-completions endpoint."),
		TEXT("CoachEndpointInput"));
	CoachModelInput = AddTextSetting(
		Parent,
		TEXT("Model"),
		TEXT("Optional provider model identifier."),
		TEXT("CoachModelInput"));
	CoachInstructionsInput = AddTextSetting(
		Parent,
		TEXT("Coach instructions"),
		TEXT("Bounded system guidance sent with AI requests. Live puzzle state is never invented."),
		TEXT("CoachInstructionsInput"));
	CoachTemperatureSlider = AddSliderRow(
		Parent,
		TEXT("Response creativity"),
		TEXT("Lower values favor repeatable technical guidance."),
		TEXT("CoachTemperatureSlider"),
		CoachTemperatureValue);
	CoachMaxResponseTokensSlider = AddSliderRow(
		Parent,
		TEXT("Response length"),
		TEXT("Bounds generated coach answers between 128 and 2,048 tokens."),
		TEXT("CoachMaxResponseTokensSlider"),
		CoachMaxResponseTokensValue);
	CoachRequestTimeoutSlider = AddSliderRow(
		Parent,
		TEXT("Request timeout"),
		TEXT("Stops an unavailable coach request after 5 to 60 seconds."),
		TEXT("CoachRequestTimeoutSlider"),
		CoachRequestTimeoutValue);
	CoachCredentialInput = AddTextSetting(
		Parent,
		TEXT("API key"),
		TEXT("Leave blank to keep the currently protected key."),
		TEXT("CoachCredentialInput"),
		true);
	UHorizontalBox* CoachCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("CoachCredentialButtonRow"));
	Parent->AddChildToVerticalBox(CoachCredentialRow);
	UButton* CoachSaveButton = AddActionButton(
		CoachCredentialRow,
		TEXT("Save protected key"),
		TEXT("CoachCredentialSaveButton"),
		true);
	UButton* CoachRemoveButton = AddActionButton(
		CoachCredentialRow,
		TEXT("Remove key"),
		TEXT("CoachCredentialRemoveButton"));
	UButton* CoachValidateButton = AddActionButton(
		CoachCredentialRow,
		TEXT("Check configuration"),
		TEXT("CoachConfigurationValidateButton"));
	CoachSaveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSaveCoachCredential);
	CoachRemoveButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleRemoveCoachCredential);
	CoachValidateButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleValidateCoachConfiguration);

	SpeechProviderCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechProviderChanged);
	SpeechActivationModeCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechActivationModeChanged);
	SpeechDictationDestinationCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechDictationDestinationChanged);
	SpeechLanguageCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechLanguageChanged);
	SpeechEndpointInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechEndpointCommitted);
	SpeechModelInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechModelCommitted);
	SpeechMicrophoneCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechMicrophoneChanged);
	SpeechContextInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechContextCommitted);
	SpeechCleanupCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechCleanupChanged);
	SpeechSoundFeedbackCheck->OnCheckStateChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleSpeechSoundFeedbackChanged);
	VoiceProviderCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceProviderChanged);
	VoiceEndpointInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceEndpointCommitted);
	VoiceModelInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceModelCommitted);
	VoiceIdInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceIdCommitted);
	VoiceSpeedSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleVoiceSpeedChanged);
	CoachProviderCombo->OnSelectionChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachProviderChanged);
	CoachEndpointInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachEndpointCommitted);
	CoachModelInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachModelCommitted);
	CoachInstructionsInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachInstructionsCommitted);
	CoachTemperatureSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachTemperatureChanged);
	CoachMaxResponseTokensSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachMaxResponseTokensChanged);
	CoachRequestTimeoutSlider->OnValueChanged.AddDynamic(
		this,
		&UHyperTwistSettingsPanelWidget::HandleCoachRequestTimeoutChanged);
}

UTextBlock* UHyperTwistSettingsPanelWidget::AddText(
	UVerticalBox* Parent,
	const FString& Text,
	const int32 FontSize,
	const FLinearColor& Color,
	const FName& WidgetName
)
{
	if (Parent == nullptr || WidgetTree == nullptr)
	{
		return nullptr;
	}
	UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass(),
		WidgetName);
	TextBlock->SetText(FText::FromString(Text));
	TextBlock->SetAutoWrapText(true);
	TextBlock->SetColorAndOpacity(FSlateColor(Color));
	FSlateFontInfo Font = TextBlock->GetFont();
	Font.Size = FontSize;
	TextBlock->SetFont(Font);
	if (UVerticalBoxSlot* TextSlot = Parent->AddChildToVerticalBox(TextBlock))
	{
		TextSlot->SetPadding(FMargin(0.0f, 2.0f));
	}
	return TextBlock;
}

void UHyperTwistSettingsPanelWidget::AddSectionIntro(
	UVerticalBox* Parent,
	const FString& Eyebrow,
	const FString& Title,
	const FString& Description
)
{
	AddText(Parent, Eyebrow, 12, AccentColor);
	AddText(Parent, Title, 24, PrimaryTextColor);
	UTextBlock* DescriptionText = AddText(Parent, Description, 14, MutedTextColor);
	if (UVerticalBoxSlot* DescriptionSlot = Cast<UVerticalBoxSlot>(
			DescriptionText != nullptr ? DescriptionText->Slot : nullptr))
	{
		DescriptionSlot->SetPadding(FMargin(0.0f, 3.0f, 0.0f, 18.0f));
	}
}

UButton* UHyperTwistSettingsPanelWidget::AddTabButton(
	UUniformGridPanel* Parent,
	const FString& Label,
	const int32 Column,
	const FName& WidgetName
)
{
	UButton* Button = WidgetTree->ConstructWidget<UButton>(
		UButton::StaticClass(),
		WidgetName);
	Button->SetBackgroundColor(CardColor);
	UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass());
	LabelText->SetText(FText::FromString(Label));
	LabelText->SetJustification(ETextJustify::Center);
	LabelText->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
	FSlateFontInfo Font = LabelText->GetFont();
	Font.Size = 13;
	LabelText->SetFont(Font);
	Button->AddChild(LabelText);
	if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(LabelText->Slot))
	{
		ButtonSlot->SetPadding(FMargin(10.0f, 8.0f));
		ButtonSlot->SetHorizontalAlignment(HAlign_Center);
	}
	if (UUniformGridSlot* GridSlot = Parent->AddChildToUniformGrid(Button, 0, Column))
	{
		GridSlot->SetHorizontalAlignment(HAlign_Fill);
	}
	return Button;
}

UButton* UHyperTwistSettingsPanelWidget::AddActionButton(
	UHorizontalBox* Parent,
	const FString& Label,
	const FName& WidgetName,
	const bool bPrimary
)
{
	UButton* Button = WidgetTree->ConstructWidget<UButton>(
		UButton::StaticClass(),
		WidgetName);
	Button->SetBackgroundColor(bPrimary ? AccentColor : CardColor);
	UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass());
	LabelText->SetText(FText::FromString(Label));
	LabelText->SetColorAndOpacity(FSlateColor(
		bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : PrimaryTextColor));
	FSlateFontInfo Font = LabelText->GetFont();
	Font.Size = 14;
	LabelText->SetFont(Font);
	Button->AddChild(LabelText);
	if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(LabelText->Slot))
	{
		ButtonSlot->SetPadding(FMargin(18.0f, 9.0f));
		ButtonSlot->SetHorizontalAlignment(HAlign_Center);
	}
	if (UHorizontalBoxSlot* ActionSlot = Parent->AddChildToHorizontalBox(Button))
	{
		ActionSlot->SetPadding(FMargin(4.0f));
		ActionSlot->SetVerticalAlignment(VAlign_Center);
	}
	return Button;
}

USlider* UHyperTwistSettingsPanelWidget::AddSliderRow(
	UVerticalBox* Parent,
	const FString& Label,
	const FString& SupportingText,
	const FName& WidgetName,
	TObjectPtr<UTextBlock>& OutValueLabel
)
{
	UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
	{
		RowSlot->SetPadding(FMargin(0.0f, 8.0f));
	}
	UHorizontalBox* Header = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass());
	Row->AddChildToVerticalBox(Header);
	UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass());
	LabelText->SetText(FText::FromString(Label));
	LabelText->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
	FSlateFontInfo Font = LabelText->GetFont();
	Font.Size = 15;
	LabelText->SetFont(Font);
	if (UHorizontalBoxSlot* LabelSlot = Header->AddChildToHorizontalBox(LabelText))
	{
		LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}
	OutValueLabel = WidgetTree->ConstructWidget<UTextBlock>(
		UTextBlock::StaticClass());
	OutValueLabel->SetColorAndOpacity(FSlateColor(AccentColor));
	if (UHorizontalBoxSlot* ValueSlot = Header->AddChildToHorizontalBox(OutValueLabel))
	{
		ValueSlot->SetHorizontalAlignment(HAlign_Right);
	}
	AddText(Row, SupportingText, 12, MutedTextColor);
	USlider* Slider = WidgetTree->ConstructWidget<USlider>(
		USlider::StaticClass(),
		WidgetName);
	Slider->SetMinValue(0.0f);
	Slider->SetMaxValue(1.0f);
	Slider->SetStepSize(0.01f);
	if (UVerticalBoxSlot* SliderSlot = Row->AddChildToVerticalBox(Slider))
	{
		SliderSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
	}
	return Slider;
}

UCheckBox* UHyperTwistSettingsPanelWidget::AddCheckRow(
	UVerticalBox* Parent,
	const FString& Label,
	const FString& SupportingText,
	const FName& WidgetName
)
{
	UHorizontalBox* Row = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass());
	if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
	{
		RowSlot->SetPadding(FMargin(0.0f, 8.0f));
	}
	UVerticalBox* Copy = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	if (UHorizontalBoxSlot* CopySlot = Row->AddChildToHorizontalBox(Copy))
	{
		CopySlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}
	AddText(Copy, Label, 15, PrimaryTextColor);
	AddText(Copy, SupportingText, 12, MutedTextColor);
	UCheckBox* CheckBox = WidgetTree->ConstructWidget<UCheckBox>(
		UCheckBox::StaticClass(),
		WidgetName);
	if (UHorizontalBoxSlot* CheckSlot = Row->AddChildToHorizontalBox(CheckBox))
	{
		CheckSlot->SetPadding(FMargin(18.0f, 0.0f));
		CheckSlot->SetVerticalAlignment(VAlign_Center);
	}
	return CheckBox;
}

UEditableTextBox* UHyperTwistSettingsPanelWidget::AddTextSetting(
	UVerticalBox* Parent,
	const FString& Label,
	const FString& SupportingText,
	const FName& WidgetName,
	const bool bPassword
)
{
	UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
	{
		RowSlot->SetPadding(FMargin(0.0f, 8.0f));
	}
	AddText(Row, Label, 15, PrimaryTextColor);
	AddText(Row, SupportingText, 12, MutedTextColor);
	UEditableTextBox* Input = WidgetTree->ConstructWidget<UEditableTextBox>(
		UEditableTextBox::StaticClass(),
		WidgetName);
	Input->SetIsPassword(bPassword);
	Input->SetForegroundColor(PrimaryTextColor);
	if (UVerticalBoxSlot* InputSlot = Row->AddChildToVerticalBox(Input))
	{
		InputSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
	}
	return Input;
}

UComboBoxString* UHyperTwistSettingsPanelWidget::AddComboSetting(
	UVerticalBox* Parent,
	const FString& Label,
	const FString& SupportingText,
	const FName& WidgetName,
	const TArray<FString>& Options
)
{
	UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
	{
		RowSlot->SetPadding(FMargin(0.0f, 8.0f));
	}
	AddText(Row, Label, 15, PrimaryTextColor);
	AddText(Row, SupportingText, 12, MutedTextColor);
	UComboBoxString* Combo = WidgetTree->ConstructWidget<UComboBoxString>(
		UComboBoxString::StaticClass(),
		WidgetName);
	for (const FString& Option : Options)
	{
		Combo->AddOption(Option);
	}
	if (UVerticalBoxSlot* ComboSlot = Row->AddChildToVerticalBox(Combo))
	{
		ComboSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
	}
	return Combo;
}

UVerticalBox* UHyperTwistSettingsPanelWidget::AddScrollablePage(
	const FName& WidgetName
)
{
	UScrollBox* Scroll = WidgetTree->ConstructWidget<UScrollBox>(
		UScrollBox::StaticClass(),
		WidgetName);
	SettingsPageSwitcher->AddChild(Scroll);
	UVerticalBox* Page = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	Scroll->AddChild(Page);
	return Page;
}

void UHyperTwistSettingsPanelWidget::RefreshWidgetsFromPreferences()
{
	if (WidgetTree == nullptr || WidgetTree->RootWidget == nullptr)
	{
		return;
	}
	bRefreshingWidgets = true;

	if (KeyboardProfileCombo != nullptr)
	{
		KeyboardProfileCombo->SetSelectedOption(
			UHyperTwistPlayerSettingsLibrary::GetKeyboardProfileDisplayName(
				Preferences.KeyboardProfileId));
	}
	if (PointerSensitivitySlider != nullptr)
	{
		PointerSensitivitySlider->SetValue((Preferences.PointerSensitivity - 0.2f) / 2.8f);
	}
	if (OrbitSensitivitySlider != nullptr)
	{
		OrbitSensitivitySlider->SetValue((Preferences.OrbitSensitivity - 0.2f) / 2.8f);
	}
	if (ZoomSensitivitySlider != nullptr)
	{
		ZoomSensitivitySlider->SetValue((Preferences.ZoomSensitivity - 0.2f) / 2.8f);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		PointerSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.PointerSensitivity));
	HyperTwistSettingsPanelWidgetInternal::SetText(
		OrbitSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.OrbitSensitivity));
	HyperTwistSettingsPanelWidgetInternal::SetText(
		ZoomSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.ZoomSensitivity));
	if (InvertOrbitCheck != nullptr)
	{
		InvertOrbitCheck->SetIsChecked(Preferences.bInvertOrbitY);
	}
	if (TouchInputCheck != nullptr)
	{
		TouchInputCheck->SetIsChecked(Preferences.bTouchInputEnabled);
	}

	const TArray<TPair<USlider*, float>> AudioSliders = {
		{MasterVolumeSlider, Preferences.MasterVolume},
		{MusicVolumeSlider, Preferences.MusicVolume},
		{EffectsVolumeSlider, Preferences.EffectsVolume},
		{VoiceVolumeSlider, Preferences.VoiceVolume}
	};
	for (const TPair<USlider*, float>& Entry : AudioSliders)
	{
		if (Entry.Key != nullptr)
		{
			Entry.Key->SetValue(Entry.Value);
		}
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		MasterVolumeValue,
		HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.MasterVolume));
	HyperTwistSettingsPanelWidgetInternal::SetText(
		MusicVolumeValue,
		HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.MusicVolume));
	HyperTwistSettingsPanelWidgetInternal::SetText(
		EffectsVolumeValue,
		HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.EffectsVolume));
	HyperTwistSettingsPanelWidgetInternal::SetText(
		VoiceVolumeValue,
		HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.VoiceVolume));
	if (MutedCheck != nullptr)
	{
		MutedCheck->SetIsChecked(Preferences.bMuted);
	}

	if (GraphicsQualityCombo != nullptr)
	{
		GraphicsQualityCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::QualityLabel(Preferences.GraphicsQuality));
	}
	if (WindowModeCombo != nullptr)
	{
		WindowModeCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::WindowModeLabel(Preferences.WindowMode));
	}
	if (ResolutionScaleSlider != nullptr)
	{
		ResolutionScaleSlider->SetValue((Preferences.ResolutionScalePercent - 50.0f) / 50.0f);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		ResolutionScaleValue,
		FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.ResolutionScalePercent)));
	if (VSyncCheck != nullptr)
	{
		VSyncCheck->SetIsChecked(Preferences.bVSyncEnabled);
	}

	if (UiScaleSlider != nullptr)
	{
		UiScaleSlider->SetValue((Preferences.UiScale - 0.8f) / 0.6f);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		UiScaleValue,
		FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.UiScale * 100.0f)));
	if (ReducedMotionCheck != nullptr)
	{
		ReducedMotionCheck->SetIsChecked(Preferences.bReducedMotion);
	}
	if (HighContrastCheck != nullptr)
	{
		HighContrastCheck->SetIsChecked(Preferences.bHighContrast);
	}
	if (SubtitlesCheck != nullptr)
	{
		SubtitlesCheck->SetIsChecked(Preferences.bSubtitlesEnabled);
	}

	if (SpeechInputCheck != nullptr)
	{
		SpeechInputCheck->SetIsChecked(Preferences.bSpeechInputEnabled);
	}
	if (CoachNarrationCheck != nullptr)
	{
		CoachNarrationCheck->SetIsChecked(Preferences.bCoachNarrationEnabled);
	}
	if (CloudProvidersCheck != nullptr)
	{
		CloudProvidersCheck->SetIsChecked(Preferences.bAllowCloudProviders);
	}
	if (AssistantPanelEnabledCheck != nullptr)
	{
		AssistantPanelEnabledCheck->SetIsChecked(Preferences.bAssistantPanelEnabled);
	}
	if (AssistantPanelOpenByDefaultCheck != nullptr)
	{
		AssistantPanelOpenByDefaultCheck->SetIsChecked(
			Preferences.bAssistantPanelOpenByDefault);
	}
	if (CoachAiEnabledCheck != nullptr)
	{
		CoachAiEnabledCheck->SetIsChecked(Preferences.bCoachAiEnabled);
	}
	if (SpeechProviderCombo != nullptr)
	{
		SpeechProviderCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
				HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions(),
				Preferences.SpeechProviderId));
	}
	if (SpeechActivationModeCombo != nullptr)
	{
		SpeechActivationModeCombo->SetSelectedOption(
			Preferences.SpeechActivationMode == TEXT("toggle")
				? TEXT("Toggle recording")
					: TEXT("Hold to talk"));
	}
	if (SpeechDictationDestinationCombo != nullptr)
	{
		SpeechDictationDestinationCombo->SetSelectedOption(
			Preferences.SpeechDictationDestination == TEXT("coach-send")
				? TEXT("Ask coach immediately")
				: TEXT("Coach draft"));
	}
	if (SpeechLanguageCombo != nullptr)
	{
		SpeechLanguageCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::SpeechLanguageLabel(
				Preferences.SpeechLanguage));
	}
	if (SpeechEndpointInput != nullptr)
	{
		SpeechEndpointInput->SetText(FText::FromString(Preferences.SpeechEndpoint));
	}
	if (SpeechModelInput != nullptr)
	{
		SpeechModelInput->SetText(FText::FromString(Preferences.SpeechModel));
	}
	if (SpeechMicrophoneCombo != nullptr)
	{
		const FHyperTwistMicrophoneDescriptor* SelectedMicrophone =
			AvailableMicrophones.FindByPredicate(
				[this](const FHyperTwistMicrophoneDescriptor& Microphone)
				{
					return Microphone.DeviceId.Equals(
						Preferences.SpeechMicrophoneId,
						ESearchCase::IgnoreCase);
				});
		const FString SelectedLabel = SelectedMicrophone != nullptr
			? SelectedMicrophone->DisplayName
			: Preferences.SpeechMicrophoneId;
		if (SpeechMicrophoneCombo->FindOptionIndex(SelectedLabel) == INDEX_NONE)
		{
			SpeechMicrophoneCombo->AddOption(SelectedLabel);
		}
		SpeechMicrophoneCombo->SetSelectedOption(SelectedLabel);
	}
	if (SpeechContextInput != nullptr)
	{
		SpeechContextInput->SetText(
			FText::FromString(Preferences.SpeechRecognitionContext));
	}
	if (SpeechCleanupCheck != nullptr)
	{
		SpeechCleanupCheck->SetIsChecked(Preferences.bSpeechCleanupEnabled);
	}
	if (SpeechSoundFeedbackCheck != nullptr)
	{
		SpeechSoundFeedbackCheck->SetIsChecked(
			Preferences.bSpeechSoundFeedbackEnabled);
	}
	if (VoiceProviderCombo != nullptr)
	{
		VoiceProviderCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
				HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions(),
				Preferences.VoiceProviderId));
	}
	if (VoiceEndpointInput != nullptr)
	{
		VoiceEndpointInput->SetText(FText::FromString(Preferences.VoiceEndpoint));
	}
	if (VoiceModelInput != nullptr)
	{
		VoiceModelInput->SetText(FText::FromString(Preferences.VoiceModel));
	}
	if (VoiceIdInput != nullptr)
	{
		VoiceIdInput->SetText(FText::FromString(Preferences.VoiceId));
	}
	if (VoiceSpeedSlider != nullptr)
	{
		VoiceSpeedSlider->SetValue((Preferences.VoiceSpeed - 0.5f) / 1.5f);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		VoiceSpeedValue,
		FString::Printf(TEXT("%.2fx"), Preferences.VoiceSpeed));
	if (CoachProviderCombo != nullptr)
	{
		CoachProviderCombo->SetSelectedOption(
			HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
				HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions(),
				Preferences.CoachProviderId));
	}
	if (CoachEndpointInput != nullptr)
	{
		CoachEndpointInput->SetText(FText::FromString(Preferences.CoachEndpoint));
	}
	if (CoachModelInput != nullptr)
	{
		CoachModelInput->SetText(FText::FromString(Preferences.CoachModel));
	}
	if (CoachInstructionsInput != nullptr)
	{
		CoachInstructionsInput->SetText(
			FText::FromString(Preferences.CoachSystemInstructions));
	}
	if (CoachTemperatureSlider != nullptr)
	{
		CoachTemperatureSlider->SetValue(Preferences.CoachTemperature);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		CoachTemperatureValue,
		FString::Printf(TEXT("%.2f"), Preferences.CoachTemperature));
	if (CoachMaxResponseTokensSlider != nullptr)
	{
		CoachMaxResponseTokensSlider->SetValue(
			static_cast<float>(Preferences.CoachMaxResponseTokens - 128)
				/ static_cast<float>(2048 - 128));
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		CoachMaxResponseTokensValue,
		FString::Printf(TEXT("%d tokens"), Preferences.CoachMaxResponseTokens));
	if (CoachRequestTimeoutSlider != nullptr)
	{
		CoachRequestTimeoutSlider->SetValue(
			(Preferences.CoachRequestTimeoutSeconds - 5.0f) / 55.0f);
	}
	HyperTwistSettingsPanelWidgetInternal::SetText(
		CoachRequestTimeoutValue,
		FString::Printf(
			TEXT("%.0f seconds"),
			Preferences.CoachRequestTimeoutSeconds));
	RefreshBindingRows();
	bRefreshingWidgets = false;
	RefreshProviderReadiness();
}

void UHyperTwistSettingsPanelWidget::RefreshBindingRows()
{
	const TArray<FHyperTwistKeyBindingDescriptor> Descriptors =
		UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(Preferences);
	for (UHyperTwistKeyBindingRowWidget* Row : BindingRows)
	{
		if (Row == nullptr)
		{
			continue;
		}
		const int32 RowIndex = BindingRows.IndexOfByKey(Row);
		if (Descriptors.IsValidIndex(RowIndex))
		{
			Row->ConfigureBinding(
				Descriptors[RowIndex].ActionId,
				Descriptors[RowIndex].Label,
				Descriptors[RowIndex].Category,
				Descriptors[RowIndex].Key);
		}
	}
}

void UHyperTwistSettingsPanelWidget::RefreshMicrophoneOptions()
{
	if (SpeechMicrophoneCombo == nullptr)
	{
		return;
	}

	const bool bWasRefreshing = bRefreshingWidgets;
	bRefreshingWidgets = true;
	AvailableMicrophones =
		UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones();
	if (!AvailableMicrophones.ContainsByPredicate(
			[this](const FHyperTwistMicrophoneDescriptor& Microphone)
			{
				return Microphone.DeviceId.Equals(
					Preferences.SpeechMicrophoneId,
					ESearchCase::IgnoreCase);
			}))
	{
		FHyperTwistMicrophoneDescriptor ExistingSelection;
		ExistingSelection.DeviceId = Preferences.SpeechMicrophoneId;
		ExistingSelection.DisplayName = Preferences.SpeechMicrophoneId;
		AvailableMicrophones.Add(MoveTemp(ExistingSelection));
	}

	SpeechMicrophoneCombo->ClearOptions();
	FString SelectedLabel;
	for (const FHyperTwistMicrophoneDescriptor& Microphone : AvailableMicrophones)
	{
		SpeechMicrophoneCombo->AddOption(Microphone.DisplayName);
		if (Microphone.DeviceId.Equals(
				Preferences.SpeechMicrophoneId,
				ESearchCase::IgnoreCase))
		{
			SelectedLabel = Microphone.DisplayName;
		}
	}
	SpeechMicrophoneCombo->SetSelectedOption(
		SelectedLabel.IsEmpty()
			? TEXT("System default microphone")
			: SelectedLabel);
	bRefreshingWidgets = bWasRefreshing;
}

void UHyperTwistSettingsPanelWidget::RefreshProviderReadiness()
{
	auto SetReadiness = [this](
		UTextBlock* Target,
		const FString& Message,
		const bool bReady)
	{
		HyperTwistSettingsPanelWidgetInternal::SetText(Target, Message);
		if (Target != nullptr)
		{
			Target->SetColorAndOpacity(FSlateColor(
				bReady ? AccentColor : WarmAccentColor));
		}
	};

	FString FailureReason;
	const bool bSpeechEndpointAllowed =
		UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.SpeechEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason);
	const bool bSpeechCloud =
		Preferences.SpeechProviderId == TEXT("openai")
		|| Preferences.SpeechProviderId == TEXT("groq")
		|| Preferences.SpeechProviderId == TEXT("deepgram")
		|| Preferences.SpeechProviderId == TEXT("elevenlabs");
	const bool bSpeechReady =
		Preferences.bSpeechInputEnabled
		&& bSpeechEndpointAllowed
		&& !Preferences.SpeechModel.IsEmpty()
		&& (!bSpeechCloud
			|| (Preferences.bAllowCloudProviders
				&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
					TEXT("speech-api-key"))));
	SetReadiness(
		SpeechReadinessText,
		bSpeechReady
			? TEXT("READY TO TRY  |  Shortcut, microphone, model, and privacy route configured.")
			: Preferences.bSpeechInputEnabled
				? TEXT("NEEDS SETUP  |  Complete the endpoint, model, consent, or protected key.")
				: TEXT("OFF  |  Enable speech-to-text to register the global shortcut."),
		bSpeechReady);

	FailureReason.Reset();
	const bool bVoiceEndpointAllowed =
		UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.VoiceEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason);
	const bool bVoiceCloud =
		Preferences.VoiceProviderId == TEXT("openai")
		|| Preferences.VoiceProviderId == TEXT("elevenlabs");
	const bool bVoiceReady =
		Preferences.bCoachNarrationEnabled
		&& bVoiceEndpointAllowed
		&& !Preferences.VoiceModel.IsEmpty()
		&& !Preferences.VoiceId.IsEmpty()
		&& (!bVoiceCloud
			|| (Preferences.bAllowCloudProviders
				&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
					TEXT("voice-api-key"))));
	SetReadiness(
		VoiceReadinessText,
		bVoiceReady
			? TEXT("READY TO TRY  |  Voice, model, endpoint, and privacy route configured.")
			: Preferences.bCoachNarrationEnabled
				? TEXT("NEEDS SETUP  |  Complete the voice, model, endpoint, consent, or protected key.")
				: TEXT("OFF  |  Enable coach narration when spoken guidance is wanted."),
		bVoiceReady);

	const bool bBuiltInCoach =
		!Preferences.bCoachAiEnabled
		|| Preferences.CoachProviderId == TEXT("local-disabled");
	FailureReason.Reset();
	const bool bCoachEndpointAllowed = bBuiltInCoach
		|| UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.CoachEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason);
	const bool bCoachCloud =
		Preferences.CoachProviderId == TEXT("openai")
		|| Preferences.CoachProviderId == TEXT("groq");
	const bool bCoachReady = bBuiltInCoach
		|| (bCoachEndpointAllowed
			&& !Preferences.CoachModel.IsEmpty()
			&& (!bCoachCloud
				|| (Preferences.bAllowCloudProviders
					&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
						TEXT("coach-api-key")))));
	SetReadiness(
		CoachReadinessText,
		bBuiltInCoach
			? TEXT("READY  |  Private built-in guide; no provider or account required.")
			: bCoachReady
				? TEXT("READY TO TRY  |  AI model, endpoint, consent, and protected key configured.")
				: TEXT("NEEDS SETUP  |  Complete the AI model, endpoint, consent, or protected key."),
		bCoachReady);
}

void UHyperTwistSettingsPanelWidget::SavePreferencesAndApply(
	const FString& StatusMessage,
	const bool bApplyGraphics
)
{
	if (!UHyperTwistPlayerSettingsLibrary::SavePreferences(Preferences))
	{
		SetStatus(TEXT("HyperTwist could not save settings on this device."), true);
		return;
	}
	UHyperTwistPlayerSettingsLibrary::ApplyRuntimePreferences(
		Preferences,
		bApplyGraphics);
	RefreshProviderReadiness();
	SetStatus(StatusMessage);
}

void UHyperTwistSettingsPanelWidget::SetStatus(
	const FString& Message,
	const bool bError
)
{
	if (StatusText != nullptr)
	{
		StatusText->SetText(FText::FromString(Message));
		StatusText->SetColorAndOpacity(FSlateColor(
			bError ? FLinearColor(1.0f, 0.38f, 0.30f, 1.0f) : MutedTextColor));
	}
}

void UHyperTwistSettingsPanelWidget::ShowControlsPage()
{
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::ControlsPageIndex);
}

void UHyperTwistSettingsPanelWidget::ShowAudioPage()
{
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::AudioPageIndex);
}

void UHyperTwistSettingsPanelWidget::ShowGraphicsPage()
{
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::GraphicsPageIndex);
}

void UHyperTwistSettingsPanelWidget::ShowAccessibilityPage()
{
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::AccessibilityPageIndex);
}

void UHyperTwistSettingsPanelWidget::ShowSpeechAndCoachPage()
{
	SettingsPageSwitcher->SetActiveWidgetIndex(
		HyperTwistSettingsPanelWidgetInternal::SpeechPageIndex);
}

void UHyperTwistSettingsPanelWidget::HandleCloseClicked()
{
	OnCloseRequested.Broadcast();
}

void UHyperTwistSettingsPanelWidget::HandleResetDefaultsClicked()
{
	Preferences = UHyperTwistPlayerSettingsLibrary::GetDefaultPreferences();
	RefreshWidgetsFromPreferences();
	SavePreferencesAndApply(TEXT("All settings restored to defaults."), true);
}

void UHyperTwistSettingsPanelWidget::HandleBindingRequested(const FName ActionId)
{
	PendingRebindActionId = ActionId;
	SetStatus(TEXT("Press a keyboard or mouse key. Escape cancels."));
	SetKeyboardFocus();
}

void UHyperTwistSettingsPanelWidget::HandleKeyboardProfileChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	FString ProfileId;
	if (SelectedItem == TEXT("WCA Pro"))
	{
		ProfileId = TEXT("classic-wca-keyboard/v1");
	}
	else if (SelectedItem == TEXT("Mnemonic Pairs"))
	{
		ProfileId = TEXT("classic-mnemonic-pairs/v1");
	}
	else
	{
		return;
	}
	if (UHyperTwistPlayerSettingsLibrary::ApplyKeyboardProfile(Preferences, ProfileId))
	{
		RefreshBindingRows();
		SavePreferencesAndApply(FString::Printf(TEXT("%s layout applied."), *SelectedItem));
	}
}

void UHyperTwistSettingsPanelWidget::HandlePointerSensitivityChanged(const float Value)
{
	if (bRefreshingWidgets)
	{
		return;
	}
	Preferences.PointerSensitivity = 0.2f + Value * 2.8f;
	HyperTwistSettingsPanelWidgetInternal::SetText(
		PointerSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.PointerSensitivity));
	SavePreferencesAndApply(TEXT("Pointer sensitivity updated."));
}

void UHyperTwistSettingsPanelWidget::HandleOrbitSensitivityChanged(const float Value)
{
	if (bRefreshingWidgets)
	{
		return;
	}
	Preferences.OrbitSensitivity = 0.2f + Value * 2.8f;
	HyperTwistSettingsPanelWidgetInternal::SetText(
		OrbitSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.OrbitSensitivity));
	SavePreferencesAndApply(TEXT("Orbit sensitivity updated."));
}

void UHyperTwistSettingsPanelWidget::HandleZoomSensitivityChanged(const float Value)
{
	if (bRefreshingWidgets)
	{
		return;
	}
	Preferences.ZoomSensitivity = 0.2f + Value * 2.8f;
	HyperTwistSettingsPanelWidgetInternal::SetText(
		ZoomSensitivityValue,
		HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.ZoomSensitivity));
	SavePreferencesAndApply(TEXT("Zoom sensitivity updated."));
}

void UHyperTwistSettingsPanelWidget::HandleInvertOrbitChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bInvertOrbitY = bChecked;
		SavePreferencesAndApply(TEXT("Orbit direction updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleTouchInputChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bTouchInputEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Touch input preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleMasterVolumeChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.MasterVolume = Value;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			MasterVolumeValue,
			HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
		SavePreferencesAndApply(TEXT("Master volume updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleMusicVolumeChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.MusicVolume = Value;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			MusicVolumeValue,
			HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
		SavePreferencesAndApply(TEXT("Music volume updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleEffectsVolumeChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.EffectsVolume = Value;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			EffectsVolumeValue,
			HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
		SavePreferencesAndApply(TEXT("Puzzle effects volume updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleVoiceVolumeChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.VoiceVolume = Value;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			VoiceVolumeValue,
			HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
		SavePreferencesAndApply(TEXT("Coach voice volume updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleMutedChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bMuted = bChecked;
		SavePreferencesAndApply(bChecked ? TEXT("Sound muted.") : TEXT("Sound restored."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleGraphicsQualityChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	Preferences.GraphicsQuality = SelectedItem == TEXT("Low")
		? EHyperTwistGraphicsQuality::Low
		: SelectedItem == TEXT("Medium")
			? EHyperTwistGraphicsQuality::Medium
			: SelectedItem == TEXT("Epic")
				? EHyperTwistGraphicsQuality::Epic
				: EHyperTwistGraphicsQuality::High;
	SavePreferencesAndApply(TEXT("Graphics quality applied."), true);
}

void UHyperTwistSettingsPanelWidget::HandleWindowModeChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	Preferences.WindowMode = SelectedItem == TEXT("Fullscreen")
		? EHyperTwistWindowMode::Fullscreen
		: SelectedItem == TEXT("Windowed")
			? EHyperTwistWindowMode::Windowed
			: EHyperTwistWindowMode::Borderless;
	SavePreferencesAndApply(TEXT("Display mode applied."), true);
}

void UHyperTwistSettingsPanelWidget::HandleResolutionScaleChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.ResolutionScalePercent = 50.0f + Value * 50.0f;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			ResolutionScaleValue,
			FString::Printf(
				TEXT("%d%%"),
				FMath::RoundToInt(Preferences.ResolutionScalePercent)));
		SavePreferencesAndApply(TEXT("Render scale applied."), true);
	}
}

void UHyperTwistSettingsPanelWidget::HandleVSyncChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bVSyncEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Vertical sync preference applied."), true);
	}
}

void UHyperTwistSettingsPanelWidget::HandleUiScaleChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.UiScale = 0.8f + Value * 0.6f;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			UiScaleValue,
			FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.UiScale * 100.0f)));
		SavePreferencesAndApply(TEXT("Interface scale applied."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleReducedMotionChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bReducedMotion = bChecked;
		SavePreferencesAndApply(TEXT("Motion preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleHighContrastChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bHighContrast = bChecked;
		SavePreferencesAndApply(TEXT("Contrast preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSubtitlesChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bSubtitlesEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Subtitle preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechInputChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bSpeechInputEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Speech input preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachNarrationChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bCoachNarrationEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Coach narration preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCloudProvidersChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bAllowCloudProviders = bChecked;
		SavePreferencesAndApply(TEXT("Cloud provider preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleAssistantPanelEnabledChanged(
	const bool bChecked
)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bAssistantPanelEnabled = bChecked;
		if (!bChecked)
		{
			Preferences.bAssistantPanelOpenByDefault = false;
		}
		SavePreferencesAndApply(TEXT("In-game coach availability updated."));
		RefreshWidgetsFromPreferences();
	}
}

void UHyperTwistSettingsPanelWidget::HandleAssistantPanelOpenByDefaultChanged(
	const bool bChecked
)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bAssistantPanelOpenByDefault = bChecked;
		if (bChecked)
		{
			Preferences.bAssistantPanelEnabled = true;
		}
		SavePreferencesAndApply(TEXT("Coach startup visibility updated."));
		RefreshWidgetsFromPreferences();
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachAiEnabledChanged(const bool bChecked)
{
	if (bRefreshingWidgets)
	{
		return;
	}
	if (bChecked
		&& Preferences.CoachProviderId.Equals(
			TEXT("local-disabled"),
			ESearchCase::IgnoreCase))
	{
		Preferences.bCoachAiEnabled = false;
		SetStatus(TEXT("Choose an AI provider before enabling generated responses."), true);
		RefreshWidgetsFromPreferences();
		return;
	}
	Preferences.bCoachAiEnabled = bChecked;
	SavePreferencesAndApply(
		bChecked
			? TEXT("AI coach responses enabled.")
			: TEXT("AI responses disabled; the built-in guide remains available."));
}

void UHyperTwistSettingsPanelWidget::HandleSpeechProviderChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
		HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
			HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions(),
			SelectedItem);
	if (Option == nullptr)
	{
		return;
	}
	Preferences.SpeechProviderId = Option->Id;
	Preferences.SpeechEndpoint = Option->DefaultEndpoint;
	Preferences.SpeechModel = Option->DefaultModel;
	SavePreferencesAndApply(TEXT("Transcription provider and safe defaults updated."));
	RefreshWidgetsFromPreferences();
}

void UHyperTwistSettingsPanelWidget::HandleSpeechActivationModeChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechActivationMode =
			SelectedItem == TEXT("Toggle recording") ? TEXT("toggle") : TEXT("hold");
		SavePreferencesAndApply(TEXT("Dictation shortcut behavior updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechDictationDestinationChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechDictationDestination =
			SelectedItem == TEXT("Ask coach immediately")
				? TEXT("coach-send")
				: TEXT("coach-draft");
		SavePreferencesAndApply(TEXT("Dictation destination updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechLanguageChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechLanguage =
			HyperTwistSettingsPanelWidgetInternal::SpeechLanguageCode(SelectedItem);
		SavePreferencesAndApply(TEXT("Transcription language updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechEndpointCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechEndpoint = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Speech endpoint saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechModelCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechModel = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Transcription model saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechMicrophoneChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}

	const FHyperTwistMicrophoneDescriptor* Microphone =
		AvailableMicrophones.FindByPredicate(
			[&SelectedItem](const FHyperTwistMicrophoneDescriptor& Candidate)
			{
				return Candidate.DisplayName.Equals(
					SelectedItem,
					ESearchCase::IgnoreCase);
			});
	if (Microphone == nullptr)
	{
		return;
	}
	Preferences.SpeechMicrophoneId = Microphone->DeviceId;
	SavePreferencesAndApply(TEXT("Microphone preference saved."));
}

void UHyperTwistSettingsPanelWidget::HandleSpeechContextCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.SpeechRecognitionContext = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Recognition context saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechCleanupChanged(const bool bChecked)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bSpeechCleanupEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Dictation cleanup preference updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSpeechSoundFeedbackChanged(
	const bool bChecked
)
{
	if (!bRefreshingWidgets)
	{
		Preferences.bSpeechSoundFeedbackEnabled = bChecked;
		SavePreferencesAndApply(TEXT("Dictation sound feedback updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleVoiceProviderChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
		HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
			HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions(),
			SelectedItem);
	if (Option == nullptr)
	{
		return;
	}
	Preferences.VoiceProviderId = Option->Id;
	Preferences.VoiceEndpoint = Option->DefaultEndpoint;
	Preferences.VoiceModel = Option->DefaultModel;
	if (Preferences.VoiceProviderId == TEXT("openai"))
	{
		Preferences.VoiceId = TEXT("alloy");
	}
	else if (Preferences.VoiceProviderId == TEXT("local-piper"))
	{
		Preferences.VoiceId = TEXT("en_US-lessac-medium");
	}
	SavePreferencesAndApply(TEXT("Voice provider and safe defaults updated."));
	RefreshWidgetsFromPreferences();
}

void UHyperTwistSettingsPanelWidget::HandleVoiceEndpointCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.VoiceEndpoint = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Voice endpoint saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleVoiceModelCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.VoiceModel = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Voice model saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleVoiceIdCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.VoiceId = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Voice selection saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleVoiceSpeedChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.VoiceSpeed = 0.5f + Value * 1.5f;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			VoiceSpeedValue,
			FString::Printf(TEXT("%.2fx"), Preferences.VoiceSpeed));
		SavePreferencesAndApply(TEXT("Narration speed updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachProviderChanged(
	FString SelectedItem,
	ESelectInfo::Type SelectionType
)
{
	static_cast<void>(SelectionType);
	if (bRefreshingWidgets)
	{
		return;
	}
	const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
		HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
			HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions(),
			SelectedItem);
	if (Option == nullptr)
	{
		return;
	}
	Preferences.CoachProviderId = Option->Id;
	Preferences.CoachEndpoint = Option->DefaultEndpoint;
	Preferences.CoachModel = Option->DefaultModel;
	if (Preferences.CoachProviderId == TEXT("local-disabled"))
	{
		Preferences.bCoachAiEnabled = false;
	}
	SavePreferencesAndApply(TEXT("Coach provider and safe defaults updated."));
	RefreshWidgetsFromPreferences();
}

void UHyperTwistSettingsPanelWidget::HandleCoachEndpointCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.CoachEndpoint = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Coach endpoint saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachModelCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.CoachModel = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Coach model saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachInstructionsCommitted(
	const FText& Text,
	ETextCommit::Type CommitMethod
)
{
	static_cast<void>(CommitMethod);
	if (!bRefreshingWidgets)
	{
		Preferences.CoachSystemInstructions = Text.ToString().TrimStartAndEnd();
		SavePreferencesAndApply(TEXT("Coach instructions saved."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachTemperatureChanged(const float Value)
{
	if (!bRefreshingWidgets)
	{
		Preferences.CoachTemperature = Value;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			CoachTemperatureValue,
			FString::Printf(TEXT("%.2f"), Value));
		SavePreferencesAndApply(TEXT("Coach response style updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachMaxResponseTokensChanged(
	const float Value
)
{
	if (!bRefreshingWidgets)
	{
		Preferences.CoachMaxResponseTokens = FMath::RoundToInt(
			128.0f + FMath::Clamp(Value, 0.0f, 1.0f) * (2048.0f - 128.0f));
		HyperTwistSettingsPanelWidgetInternal::SetText(
			CoachMaxResponseTokensValue,
			FString::Printf(
				TEXT("%d tokens"),
				Preferences.CoachMaxResponseTokens));
		SavePreferencesAndApply(TEXT("Coach response length updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleCoachRequestTimeoutChanged(
	const float Value
)
{
	if (!bRefreshingWidgets)
	{
		Preferences.CoachRequestTimeoutSeconds =
			5.0f + FMath::Clamp(Value, 0.0f, 1.0f) * 55.0f;
		HyperTwistSettingsPanelWidgetInternal::SetText(
			CoachRequestTimeoutValue,
			FString::Printf(
				TEXT("%.0f seconds"),
				Preferences.CoachRequestTimeoutSeconds));
		SavePreferencesAndApply(TEXT("Coach request timeout updated."));
	}
}

void UHyperTwistSettingsPanelWidget::HandleSaveSpeechCredential()
{
	FString FailureReason;
	const FString Secret = SpeechCredentialInput != nullptr
		? SpeechCredentialInput->GetText().ToString()
		: FString();
	if (Secret.IsEmpty())
	{
		SetStatus(TEXT("Enter a speech API key, or leave the protected key unchanged."));
		return;
	}
	if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
			TEXT("speech-api-key"),
			Secret,
			FailureReason))
	{
		SpeechCredentialInput->SetText(FText::GetEmpty());
		RefreshProviderReadiness();
		SetStatus(TEXT("Speech API key protected for this Windows account."));
	}
	else
	{
		SetStatus(FailureReason, true);
	}
}

void UHyperTwistSettingsPanelWidget::HandleRemoveSpeechCredential()
{
	UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("speech-api-key"));
	if (SpeechCredentialInput != nullptr)
	{
		SpeechCredentialInput->SetText(FText::GetEmpty());
	}
	RefreshProviderReadiness();
	SetStatus(TEXT("Protected speech API key removed."));
}

void UHyperTwistSettingsPanelWidget::HandleSaveVoiceCredential()
{
	FString FailureReason;
	const FString Secret = VoiceCredentialInput != nullptr
		? VoiceCredentialInput->GetText().ToString()
		: FString();
	if (Secret.IsEmpty())
	{
		SetStatus(TEXT("Enter a voice API key, or leave the protected key unchanged."));
		return;
	}
	if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
			TEXT("voice-api-key"),
			Secret,
			FailureReason))
	{
		VoiceCredentialInput->SetText(FText::GetEmpty());
		RefreshProviderReadiness();
		SetStatus(TEXT("Voice API key protected for this Windows account."));
	}
	else
	{
		SetStatus(FailureReason, true);
	}
}

void UHyperTwistSettingsPanelWidget::HandleRemoveVoiceCredential()
{
	UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("voice-api-key"));
	if (VoiceCredentialInput != nullptr)
	{
		VoiceCredentialInput->SetText(FText::GetEmpty());
	}
	RefreshProviderReadiness();
	SetStatus(TEXT("Protected voice API key removed."));
}

void UHyperTwistSettingsPanelWidget::HandleSaveCoachCredential()
{
	FString FailureReason;
	const FString Secret = CoachCredentialInput != nullptr
		? CoachCredentialInput->GetText().ToString()
		: FString();
	if (Secret.IsEmpty())
	{
		SetStatus(TEXT("Enter a coach API key, or leave the protected key unchanged."));
		return;
	}
	if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
			TEXT("coach-api-key"),
			Secret,
			FailureReason))
	{
		CoachCredentialInput->SetText(FText::GetEmpty());
		RefreshProviderReadiness();
		SetStatus(TEXT("Coach API key protected for this Windows account."));
	}
	else
	{
		SetStatus(FailureReason, true);
	}
}

void UHyperTwistSettingsPanelWidget::HandleRemoveCoachCredential()
{
	UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("coach-api-key"));
	if (CoachCredentialInput != nullptr)
	{
		CoachCredentialInput->SetText(FText::GetEmpty());
	}
	RefreshProviderReadiness();
	SetStatus(TEXT("Protected coach API key removed."));
}

void UHyperTwistSettingsPanelWidget::HandleRefreshMicrophones()
{
	RefreshMicrophoneOptions();
	SetStatus(FString::Printf(
		TEXT("Detected %d microphone route%s."),
		AvailableMicrophones.Num(),
		AvailableMicrophones.Num() == 1 ? TEXT("") : TEXT("s")));
}

void UHyperTwistSettingsPanelWidget::HandleTestMicrophone()
{
	FString FailureReason;
	if (UHyperTwistPlayerSettingsLibrary::TestMicrophoneDevice(
			Preferences.SpeechMicrophoneId,
			FailureReason))
	{
		SetStatus(
			TEXT("Microphone opened successfully. No test audio was retained."));
		return;
	}
	SetStatus(FailureReason, true);
}

void UHyperTwistSettingsPanelWidget::HandleValidateSpeechConfiguration()
{
	FString FailureReason;
	const bool bCloudProviderSelected =
		Preferences.SpeechProviderId == TEXT("openai")
		|| Preferences.SpeechProviderId == TEXT("groq")
		|| Preferences.SpeechProviderId == TEXT("deepgram")
		|| Preferences.SpeechProviderId == TEXT("elevenlabs");
	if (bCloudProviderSelected && !Preferences.bAllowCloudProviders)
	{
		SetStatus(
			TEXT("Enable cloud providers before using the selected transcription route."),
			true);
		return;
	}
	if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.SpeechEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason))
	{
		SetStatus(FailureReason, true);
		return;
	}
	if (Preferences.SpeechModel.IsEmpty())
	{
		SetStatus(TEXT("Choose a transcription model."), true);
		return;
	}
	const bool bCloudCredentialRequired =
		Preferences.SpeechProviderId == TEXT("openai")
		|| Preferences.SpeechProviderId == TEXT("groq")
		|| Preferences.SpeechProviderId == TEXT("deepgram")
		|| Preferences.SpeechProviderId == TEXT("elevenlabs");
	if (bCloudCredentialRequired
		&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
			TEXT("speech-api-key")))
	{
		SetStatus(TEXT("Save a protected key for the selected transcription provider."), true);
		return;
	}
	SetStatus(
		TEXT("Speech configuration is complete. Service reachability is checked when dictation starts."));
}

void UHyperTwistSettingsPanelWidget::HandleValidateVoiceConfiguration()
{
	FString FailureReason;
	const bool bCloudProviderSelected =
		Preferences.VoiceProviderId == TEXT("openai")
		|| Preferences.VoiceProviderId == TEXT("elevenlabs");
	if (bCloudProviderSelected && !Preferences.bAllowCloudProviders)
	{
		SetStatus(
			TEXT("Enable cloud providers before using the selected voice route."),
			true);
		return;
	}
	if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.VoiceEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason))
	{
		SetStatus(FailureReason, true);
		return;
	}
	if (Preferences.VoiceModel.IsEmpty() || Preferences.VoiceId.IsEmpty())
	{
		SetStatus(TEXT("Choose both a voice model and voice identifier."), true);
		return;
	}
	const bool bCloudCredentialRequired =
		Preferences.VoiceProviderId == TEXT("openai")
		|| Preferences.VoiceProviderId == TEXT("elevenlabs");
	if (bCloudCredentialRequired
		&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
			TEXT("voice-api-key")))
	{
		SetStatus(TEXT("Save a protected key for the selected voice provider."), true);
		return;
	}
	SetStatus(
		TEXT("Voice configuration is complete. Service reachability is checked before narration."));
}

void UHyperTwistSettingsPanelWidget::HandleValidateCoachConfiguration()
{
	if (Preferences.CoachProviderId == TEXT("local-disabled"))
	{
		SetStatus(TEXT("The private built-in guide is ready; no AI service is configured."));
		return;
	}

	FString FailureReason;
	if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.CoachEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason))
	{
		SetStatus(FailureReason, true);
		return;
	}
	if (Preferences.CoachModel.IsEmpty())
	{
		SetStatus(TEXT("Choose a coach model before enabling AI responses."), true);
		return;
	}
	const bool bCloudCredentialRequired =
		Preferences.CoachProviderId == TEXT("openai")
		|| Preferences.CoachProviderId == TEXT("groq");
	if (bCloudCredentialRequired
		&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
			TEXT("coach-api-key")))
	{
		SetStatus(TEXT("Save a protected key for the selected coach provider."), true);
		return;
	}
	SetStatus(
		TEXT("Coach configuration is complete. The fixed assistant will verify the service on its first request."));
}
