#include "HyperTwistUX/HyperTwistCoachAssistantWidget.h"

#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/EditableTextBox.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/ScrollBox.h"
#include "Components/SizeBox.h"
#include "Components/Spacer.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Dom/JsonObject.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Input/Reply.h"
#include "InputCoreTypes.h"
#include "Misc/ScopeExit.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"

namespace HyperTwistCoachAssistantWidgetInternal
{
	const FLinearColor PanelColor(0.012f, 0.025f, 0.041f, 0.985f);
	const FLinearColor CardColor(0.035f, 0.063f, 0.088f, 1.0f);
	const FLinearColor UserCardColor(0.055f, 0.145f, 0.155f, 1.0f);
	const FLinearColor AccentColor(0.10f, 0.92f, 0.80f, 1.0f);
	const FLinearColor WarmColor(1.0f, 0.63f, 0.24f, 1.0f);
	const FLinearColor TextColor(0.93f, 0.97f, 1.0f, 1.0f);
	const FLinearColor MutedColor(0.62f, 0.72f, 0.80f, 1.0f);
	constexpr int32 MaximumPromptLength = 4000;
	constexpr int32 MaximumResponseLength = 12000;
	constexpr int32 MaximumHistoryMessageCount = 10;

	UTextBlock* MakeText(
		UWidgetTree* WidgetTree,
		const FString& Text,
		const int32 FontSize,
		const FLinearColor& Color,
		const FName& Name = NAME_None
	)
	{
		UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
			UTextBlock::StaticClass(),
			Name);
		TextBlock->SetText(FText::FromString(Text));
		TextBlock->SetAutoWrapText(true);
		TextBlock->SetColorAndOpacity(FSlateColor(Color));
		FSlateFontInfo Font = TextBlock->GetFont();
		Font.Size = FontSize;
		TextBlock->SetFont(Font);
		return TextBlock;
	}

	UButton* MakeButton(
		UWidgetTree* WidgetTree,
		const FString& Label,
		const FName& Name,
		const bool bPrimary = false
	)
	{
		UButton* Button = WidgetTree->ConstructWidget<UButton>(
			UButton::StaticClass(),
			Name);
		Button->SetBackgroundColor(bPrimary ? AccentColor : CardColor);
		UTextBlock* LabelText = MakeText(
			WidgetTree,
			Label,
			13,
			bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : TextColor);
		LabelText->SetJustification(ETextJustify::Center);
		Button->AddChild(LabelText);
		if (UButtonSlot* ContentSlot = Cast<UButtonSlot>(LabelText->Slot))
		{
			ContentSlot->SetPadding(FMargin(13.0f, 8.0f));
			ContentSlot->SetHorizontalAlignment(HAlign_Center);
		}
		return Button;
	}

	void AddJsonMessage(
		TArray<TSharedPtr<FJsonValue>>& Messages,
		const FString& Role,
		const FString& Content
	)
	{
		TSharedRef<FJsonObject> Message = MakeShared<FJsonObject>();
		Message->SetStringField(TEXT("role"), Role);
		Message->SetStringField(TEXT("content"), Content);
		Messages.Add(MakeShared<FJsonValueObject>(Message));
	}

	FString ExtractErrorMessage(const TSharedPtr<FJsonObject>& Root)
	{
		const TSharedPtr<FJsonObject>* ErrorObject = nullptr;
		if (Root.IsValid()
			&& Root->TryGetObjectField(TEXT("error"), ErrorObject)
			&& ErrorObject != nullptr
			&& ErrorObject->IsValid())
		{
			FString ErrorMessage;
			if ((*ErrorObject)->TryGetStringField(TEXT("message"), ErrorMessage))
			{
				return ErrorMessage;
			}
		}
		return FString();
	}
}

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

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

void UHyperTwistCoachAssistantWidget::NativeDestruct()
{
	if (AHyperTwistPlayerControllerBase* PlayerController =
			Cast<AHyperTwistPlayerControllerBase>(GetOwningPlayer()))
	{
		PlayerController->CancelGlobalDictation();
	}
	if (ActiveRequest.IsValid())
	{
		ActiveRequest->OnProcessRequestComplete().Unbind();
		ActiveRequest->CancelRequest();
		ActiveRequest.Reset();
	}
	bRequestInFlight = false;
	PendingUserPrompt.Reset();
	Super::NativeDestruct();
}

FReply UHyperTwistCoachAssistantWidget::NativeOnKeyDown(
	const FGeometry& InGeometry,
	const FKeyEvent& InKeyEvent
)
{
	static_cast<void>(InGeometry);
	const FKey Key = InKeyEvent.GetKey();
	if (Key == EKeys::Escape || Key == EKeys::F3)
	{
		OnCloseRequested.Broadcast();
		return FReply::Handled();
	}
	return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}

bool UHyperTwistCoachAssistantWidget::PrepareAssistantSurface()
{
	EnsureWidgetTreeBuilt();
	SetPuzzleContext(PuzzleTitle, PuzzleSubtitle);
	if (ConversationMessages.IsEmpty())
	{
		AddConversationMessage(
			TEXT("HYPERTWIST GUIDE"),
			TEXT("Ask about controls, the current puzzle family, projections, settings, or practice. The built-in guide works offline; generated AI responses are optional."),
			false);
	}
	SetDictationStatus(TEXT("Private built-in guide ready."), false, false);
	return IsAssistantSurfaceReady();
}

bool UHyperTwistCoachAssistantWidget::IsAssistantSurfaceReady() const
{
	return WidgetTree != nullptr
		&& WidgetTree->RootWidget != nullptr
		&& ConversationScroll != nullptr
		&& ConversationList != nullptr
		&& PromptInput != nullptr
		&& StatusText != nullptr;
}

void UHyperTwistCoachAssistantWidget::SetPuzzleContext(
	const FString& Title,
	const FString& Subtitle
)
{
	PuzzleTitle = Title.IsEmpty() ? TEXT("HyperTwist") : Title;
	PuzzleSubtitle = Subtitle.IsEmpty() ? TEXT("Active puzzle") : Subtitle;
	if (ContextText != nullptr)
	{
		ContextText->SetText(FText::FromString(
			FString::Printf(TEXT("%s  |  %s"), *PuzzleTitle, *PuzzleSubtitle)));
	}
}

void UHyperTwistCoachAssistantWidget::SubmitDictatedPrompt(
	const FString& Transcript,
	const bool bSubmitImmediately
)
{
	const FString SafeTranscript = Transcript.TrimStartAndEnd();
	if (SafeTranscript.IsEmpty())
	{
		SetDictationStatus(TEXT("The transcription service returned no text."), false, true);
		return;
	}

	if (bSubmitImmediately)
	{
		SubmitPrompt(SafeTranscript);
		return;
	}

	if (PromptInput != nullptr)
	{
		PromptInput->SetText(FText::FromString(SafeTranscript));
		PromptInput->SetKeyboardFocus();
	}
	SetDictationStatus(
		TEXT("Transcript ready. Review it, then select Send."),
		false,
		false);
}

void UHyperTwistCoachAssistantWidget::SetDictationStatus(
	const FString& Status,
	const bool bRecording,
	const bool bError
)
{
	SetStatus(Status, bError);
	if (DictationButtonLabel == nullptr)
	{
		return;
	}

	if (bRecording)
	{
		DictationButtonLabel->SetText(FText::FromString(TEXT("Stop dictation")));
		return;
	}

	const FHyperTwistPlayerPreferences Preferences =
		UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	const FKey DictationKey =
		UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
			Preferences,
			TEXT("speech.push-to-talk"));
	DictationButtonLabel->SetText(FText::FromString(FString::Printf(
		TEXT("Talk (%s)"),
		*DictationKey.GetDisplayName().ToString())));
}

void UHyperTwistCoachAssistantWidget::SetNarrationStatus(
	const FString& Status,
	const bool bError
)
{
	SetStatus(Status, bError);
}

void UHyperTwistCoachAssistantWidget::EnsureWidgetTreeBuilt()
{
	using namespace HyperTwistCoachAssistantWidgetInternal;
	if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
	{
		return;
	}

	UOverlay* Root = WidgetTree->ConstructWidget<UOverlay>(
		UOverlay::StaticClass(),
		TEXT("CoachAssistantRoot"));
	WidgetTree->RootWidget = Root;

	USizeBox* PanelSize = WidgetTree->ConstructWidget<USizeBox>(
		USizeBox::StaticClass(),
		TEXT("CoachAssistantPanelSize"));
	PanelSize->SetWidthOverride(480.0f);
	PanelSize->SetHeightOverride(720.0f);
	if (UOverlaySlot* PanelSizeSlot = Root->AddChildToOverlay(PanelSize))
	{
		PanelSizeSlot->SetHorizontalAlignment(HAlign_Right);
		PanelSizeSlot->SetVerticalAlignment(VAlign_Center);
		PanelSizeSlot->SetPadding(FMargin(18.0f));
	}

	UBorder* Panel = WidgetTree->ConstructWidget<UBorder>(
		UBorder::StaticClass(),
		TEXT("CoachAssistantPanel"));
	Panel->SetPadding(FMargin(22.0f));
	Panel->SetBrushColor(PanelColor);
	PanelSize->AddChild(Panel);

	UVerticalBox* Content = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass(),
		TEXT("CoachAssistantContent"));
	Panel->AddChild(Content);

	UHorizontalBox* Header = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("CoachAssistantHeader"));
	Content->AddChildToVerticalBox(Header);
	UVerticalBox* HeaderCopy = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	if (UHorizontalBoxSlot* HeaderCopySlot = Header->AddChildToHorizontalBox(HeaderCopy))
	{
		HeaderCopySlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}
	UTextBlock* Eyebrow = MakeText(WidgetTree, TEXT("IN-GAME COACH"), 11, AccentColor);
	HeaderCopy->AddChildToVerticalBox(Eyebrow);
	UTextBlock* Title = MakeText(WidgetTree, TEXT("A clear next move"), 25, TextColor);
	HeaderCopy->AddChildToVerticalBox(Title);
	ContextText = MakeText(
		WidgetTree,
		TEXT("HyperTwist  |  Active puzzle"),
		12,
		MutedColor,
		TEXT("CoachAssistantContext"));
	HeaderCopy->AddChildToVerticalBox(ContextText);

	UButton* CloseButton = MakeButton(
		WidgetTree,
		TEXT("Close"),
		TEXT("CoachAssistantCloseButton"));
	CloseButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleCloseClicked);
	if (UHorizontalBoxSlot* CloseSlot = Header->AddChildToHorizontalBox(CloseButton))
	{
		CloseSlot->SetVerticalAlignment(VAlign_Top);
		CloseSlot->SetPadding(FMargin(8.0f, 0.0f, 0.0f, 0.0f));
	}

	UHorizontalBox* QuickActions = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("CoachAssistantQuickActions"));
	if (UVerticalBoxSlot* QuickSlot = Content->AddChildToVerticalBox(QuickActions))
	{
		QuickSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 10.0f));
	}
	UButton* ControlsButton = MakeButton(
		WidgetTree,
		TEXT("Controls"),
		TEXT("CoachAssistantControlsPrompt"));
	UButton* PuzzleButton = MakeButton(
		WidgetTree,
		TEXT("This puzzle"),
		TEXT("CoachAssistantPuzzlePrompt"));
	UButton* SettingsButton = MakeButton(
		WidgetTree,
		TEXT("Settings"),
		TEXT("CoachAssistantSettingsPrompt"));
	ControlsButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleControlsPromptClicked);
	PuzzleButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandlePuzzlePromptClicked);
	SettingsButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleSettingsPromptClicked);
	for (UButton* Button : {ControlsButton, PuzzleButton, SettingsButton})
	{
		if (UHorizontalBoxSlot* ButtonSlot = QuickActions->AddChildToHorizontalBox(Button))
		{
			ButtonSlot->SetPadding(FMargin(3.0f));
			ButtonSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
		}
	}

	ConversationScroll = WidgetTree->ConstructWidget<UScrollBox>(
		UScrollBox::StaticClass(),
		TEXT("CoachAssistantConversationScroll"));
	if (UVerticalBoxSlot* ConversationSlot =
			Content->AddChildToVerticalBox(ConversationScroll))
	{
		ConversationSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
		ConversationSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 10.0f));
	}
	ConversationList = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass(),
		TEXT("CoachAssistantConversationList"));
	ConversationScroll->AddChild(ConversationList);

	StatusText = MakeText(
		WidgetTree,
		TEXT("Private built-in guide ready."),
		11,
		MutedColor,
		TEXT("CoachAssistantStatus"));
	if (UVerticalBoxSlot* StatusSlot = Content->AddChildToVerticalBox(StatusText))
	{
		StatusSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 7.0f));
	}

	PromptInput = WidgetTree->ConstructWidget<UEditableTextBox>(
		UEditableTextBox::StaticClass(),
		TEXT("CoachAssistantPrompt"));
	PromptInput->SetHintText(FText::FromString(
		TEXT("Ask about controls, projections, or your next practice step...")));
	PromptInput->SetForegroundColor(TextColor);
	PromptInput->OnTextCommitted.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandlePromptCommitted);
	if (UVerticalBoxSlot* PromptSlot = Content->AddChildToVerticalBox(PromptInput))
	{
		PromptSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 8.0f));
	}

	UHorizontalBox* ComposerActions = WidgetTree->ConstructWidget<UHorizontalBox>(
		UHorizontalBox::StaticClass(),
		TEXT("CoachAssistantComposerActions"));
	Content->AddChildToVerticalBox(ComposerActions);
	UButton* ClearButton = MakeButton(
		WidgetTree,
		TEXT("Clear"),
		TEXT("CoachAssistantClearButton"));
	DictationButton = MakeButton(
		WidgetTree,
		TEXT("Talk (V)"),
		TEXT("CoachAssistantDictationButton"));
	DictationButtonLabel = Cast<UTextBlock>(DictationButton->GetContent());
	UButton* SendButton = MakeButton(
		WidgetTree,
		TEXT("Send"),
		TEXT("CoachAssistantSendButton"),
		true);
	ClearButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleClearClicked);
	DictationButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleDictationClicked);
	SendButton->OnClicked.AddDynamic(
		this,
		&UHyperTwistCoachAssistantWidget::HandleSendClicked);
	if (UHorizontalBoxSlot* ClearSlot = ComposerActions->AddChildToHorizontalBox(ClearButton))
	{
		ClearSlot->SetPadding(FMargin(0.0f, 0.0f, 4.0f, 0.0f));
	}
	if (UHorizontalBoxSlot* DictationSlot =
			ComposerActions->AddChildToHorizontalBox(DictationButton))
	{
		DictationSlot->SetPadding(FMargin(4.0f));
	}
	if (UHorizontalBoxSlot* FillSlot = ComposerActions->AddChildToHorizontalBox(
			WidgetTree->ConstructWidget<USpacer>(USpacer::StaticClass())))
	{
		FillSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
	}
	if (UHorizontalBoxSlot* SendSlot = ComposerActions->AddChildToHorizontalBox(SendButton))
	{
		SendSlot->SetPadding(FMargin(4.0f, 0.0f, 0.0f, 0.0f));
	}

	UTextBlock* PrivacyLine = MakeText(
		WidgetTree,
		TEXT("F3 closes  |  Chat and temporary dictation audio are not retained  |  Cloud use requires consent"),
		10,
		WarmColor);
	if (UVerticalBoxSlot* PrivacySlot = Content->AddChildToVerticalBox(PrivacyLine))
	{
		PrivacySlot->SetPadding(FMargin(0.0f, 10.0f, 0.0f, 0.0f));
	}
}

void UHyperTwistCoachAssistantWidget::AddConversationMessage(
	const FString& RoleLabel,
	const FString& Message,
	const bool bUserMessage
)
{
	using namespace HyperTwistCoachAssistantWidgetInternal;
	if (ConversationList == nullptr || WidgetTree == nullptr)
	{
		return;
	}

	UBorder* Card = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass());
	Card->SetPadding(FMargin(12.0f));
	Card->SetBrushColor(bUserMessage ? UserCardColor : CardColor);
	UVerticalBox* CardContent = WidgetTree->ConstructWidget<UVerticalBox>(
		UVerticalBox::StaticClass());
	Card->AddChild(CardContent);
	UTextBlock* Role = MakeText(
		WidgetTree,
		RoleLabel,
		10,
		bUserMessage ? AccentColor : WarmColor);
	CardContent->AddChildToVerticalBox(Role);
	UTextBlock* Body = MakeText(WidgetTree, Message, 13, TextColor);
	if (UVerticalBoxSlot* BodySlot = CardContent->AddChildToVerticalBox(Body))
	{
		BodySlot->SetPadding(FMargin(0.0f, 3.0f, 0.0f, 0.0f));
	}
	if (UVerticalBoxSlot* CardSlot = ConversationList->AddChildToVerticalBox(Card))
	{
		CardSlot->SetPadding(FMargin(0.0f, 3.0f));
	}
	ConversationScroll->ScrollToEnd();
}

void UHyperTwistCoachAssistantWidget::SubmitPrompt(const FString& Prompt)
{
	using namespace HyperTwistCoachAssistantWidgetInternal;
	const FString SafePrompt = Prompt.TrimStartAndEnd();
	if (SafePrompt.IsEmpty())
	{
		SetStatus(TEXT("Type a question first."), true);
		return;
	}
	if (SafePrompt.Len() > MaximumPromptLength)
	{
		SetStatus(TEXT("Keep a coach question under 4,000 characters."), true);
		return;
	}
	if (bRequestInFlight)
	{
		SetStatus(TEXT("Wait for the current coach response."), true);
		return;
	}

	AddConversationMessage(TEXT("YOU"), SafePrompt, true);
	if (PromptInput != nullptr)
	{
		PromptInput->SetText(FText::GetEmpty());
	}

	const FHyperTwistPlayerPreferences Preferences =
		UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	if (!Preferences.bCoachAiEnabled
		|| Preferences.CoachProviderId.Equals(
			TEXT("local-disabled"),
			ESearchCase::IgnoreCase))
	{
		const FString BuiltInResponse = BuildBuiltInGuideResponse(SafePrompt);
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), BuiltInResponse, false);
		SetStatus(TEXT("Answered locally. No text left this device."));
		NarrateResponseIfEnabled(BuiltInResponse);
		return;
	}

	BeginCoachRequest(SafePrompt);
}

void UHyperTwistCoachAssistantWidget::BeginCoachRequest(const FString& Prompt)
{
	const FHyperTwistPlayerPreferences Preferences =
		UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	FString FailureReason;
	if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			Preferences.CoachEndpoint,
			Preferences.bAllowCloudProviders,
			FailureReason))
	{
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), FailureReason, false);
		SetStatus(FailureReason, true);
		return;
	}
	if (Preferences.CoachModel.IsEmpty())
	{
		const FString Message =
			TEXT("Choose a coach model in Settings > Voice & AI before using generated responses.");
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), Message, false);
		SetStatus(Message, true);
		return;
	}

	FString Credential;
	const bool bHasCredential =
		UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
			TEXT("coach-api-key"),
			Credential);
	ON_SCOPE_EXIT
	{
		if (!Credential.IsEmpty())
		{
			Credential.Reset(Credential.Len());
		}
	};
	const bool bCredentialRequired =
		Preferences.CoachProviderId == TEXT("openai")
		|| Preferences.CoachProviderId == TEXT("groq");
	if (bCredentialRequired && (!bHasCredential || Credential.IsEmpty()))
	{
		const FString Message =
			TEXT("Save a protected coach API key in Settings > Voice & AI.");
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), Message, false);
		SetStatus(Message, true);
		return;
	}

	const FString RequestUrl = BuildRequestUrl(Preferences.CoachEndpoint);
	bLastRequestUsedResponsesApi =
		RequestUrl.EndsWith(TEXT("/responses"), ESearchCase::IgnoreCase);
	const FString RequestBody =
		BuildRequestBody(Prompt, bLastRequestUsedResponsesApi);
	if (RequestUrl.IsEmpty() || RequestBody.IsEmpty())
	{
		SetStatus(TEXT("The coach request could not be prepared."), true);
		return;
	}

	ActiveRequest = FHttpModule::Get().CreateRequest();
	ActiveRequest->SetURL(RequestUrl);
	ActiveRequest->SetVerb(TEXT("POST"));
	ActiveRequest->SetHeader(TEXT("Accept"), TEXT("application/json"));
	ActiveRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	if (!Credential.IsEmpty())
	{
		ActiveRequest->SetHeader(
			TEXT("Authorization"),
			FString::Printf(TEXT("Bearer %s"), *Credential));
	}
	ActiveRequest->SetContentAsString(RequestBody);
	ActiveRequest->SetTimeout(Preferences.CoachRequestTimeoutSeconds);

	TWeakObjectPtr<UHyperTwistCoachAssistantWidget> WeakThis(this);
	ActiveRequest->OnProcessRequestComplete().BindLambda(
		[WeakThis](
			FHttpRequestPtr CompletedRequest,
			FHttpResponsePtr Response,
			const bool bSucceeded)
		{
			UHyperTwistCoachAssistantWidget* Widget = WeakThis.Get();
			if (Widget == nullptr
				|| Widget->ActiveRequest.Get() != CompletedRequest.Get())
			{
				return;
			}
			Widget->HandleCoachResponse(
				bSucceeded && Response.IsValid(),
				Response.IsValid() ? Response->GetResponseCode() : 0,
				Response.IsValid() ? Response->GetContentAsString() : FString());
		});

	bRequestInFlight = true;
	PendingUserPrompt = Prompt;
	SetStatus(TEXT("Coach is thinking..."));
	if (!ActiveRequest->ProcessRequest())
	{
		bRequestInFlight = false;
		PendingUserPrompt.Reset();
		ActiveRequest->OnProcessRequestComplete().Unbind();
		ActiveRequest.Reset();
		SetStatus(TEXT("The coach request could not start."), true);
	}
}

void UHyperTwistCoachAssistantWidget::HandleCoachResponse(
	const bool bTransportSucceeded,
	const int32 StatusCode,
	const FString& ResponseBody
)
{
	bRequestInFlight = false;
	if (ActiveRequest.IsValid())
	{
		ActiveRequest->OnProcessRequestComplete().Unbind();
		ActiveRequest.Reset();
	}

	if (!bTransportSucceeded)
	{
		PendingUserPrompt.Reset();
		const FString Message =
			TEXT("The configured coach service could not be reached. Your offline guide remains available.");
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), Message, false);
		SetStatus(Message, true);
		return;
	}

	FString FailureReason;
	const FString ResponseText = ParseResponseText(
		ResponseBody,
		bLastRequestUsedResponsesApi,
		FailureReason);
	if (StatusCode < 200 || StatusCode >= 300 || ResponseText.IsEmpty())
	{
		PendingUserPrompt.Reset();
		const FString Message = FailureReason.IsEmpty()
			? FString::Printf(TEXT("Coach service returned HTTP %d."), StatusCode)
			: FailureReason;
		AddConversationMessage(TEXT("HYPERTWIST GUIDE"), Message, false);
		SetStatus(Message, true);
		return;
	}

	AddConversationMessage(TEXT("HYPERTWIST COACH"), ResponseText, false);
	ConversationRoles.Add(TEXT("user"));
	ConversationMessages.Add(PendingUserPrompt);
	ConversationRoles.Add(TEXT("assistant"));
	ConversationMessages.Add(ResponseText);
	PendingUserPrompt.Reset();
	while (ConversationMessages.Num() > HyperTwistCoachAssistantWidgetInternal::MaximumHistoryMessageCount)
	{
		ConversationRoles.RemoveAt(0);
		ConversationMessages.RemoveAt(0);
	}
	SetStatus(TEXT("Generated response received. Chat remains session-only."));
	NarrateResponseIfEnabled(ResponseText);
}

void UHyperTwistCoachAssistantWidget::NarrateResponseIfEnabled(
	const FString& ResponseText
)
{
	if (AHyperTwistPlayerControllerBase* PlayerController =
			Cast<AHyperTwistPlayerControllerBase>(GetOwningPlayer()))
	{
		PlayerController->NarrateCoachResponse(ResponseText);
	}
}

void UHyperTwistCoachAssistantWidget::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)
				: HyperTwistCoachAssistantWidgetInternal::MutedColor));
	}
}

FString UHyperTwistCoachAssistantWidget::BuildBuiltInGuideResponse(
	const FString& Prompt
) const
{
	const FString Query = Prompt.ToLower();
	if (Query.Contains(TEXT("control"))
		|| Query.Contains(TEXT("keyboard"))
		|| Query.Contains(TEXT("mouse")))
	{
		return TEXT("Press Esc for the complete controls page. F10 returns to the Puzzle Library, F1 requests a hint where supported, F3 toggles this coach, the wheel zooms, and middle-mouse or Shift + right-mouse orbits supported puzzle views. Every binding can be changed in Settings.");
	}
	if (Query.Contains(TEXT("4d"))
		|| Query.Contains(TEXT("hypercube"))
		|| Query.Contains(TEXT("slice"))
		|| Query.Contains(TEXT("axis")))
	{
		return TEXT("In the visible-slice 4D cubes, X/Y/Z/W choose the slice axis, number keys choose a layer, brackets step through layers, Q/E cycle rotation planes, comma/period turn counter-clockwise or clockwise, R resets, T toggles the cell shell, and S/L save or restore that order. The 2x cube uses direct cell clicks plus R reset, G scramble, and the same S/L persistence.");
	}
	if (Query.Contains(TEXT("120"))
		|| Query.Contains(TEXT("5d"))
		|| Query.Contains(TEXT("dimension")))
	{
		return TEXT("The 120-cell and 5D modes use dedicated runtime families. Space toggles projection motion, Q/E rotates the projection, Page Up/Down changes layers, N creates a new state, R resets, S/L save and load, Home resets the camera, and the wheel plus middle-mouse orbit the view.");
	}
	if (Query.Contains(TEXT("setting"))
		|| Query.Contains(TEXT("speech"))
		|| Query.Contains(TEXT("voice"))
		|| Query.Contains(TEXT("api")))
	{
		return TEXT("Press Esc, choose Settings & Controls, then open Voice & AI. Speech, narration, and generated AI are global opt-in capabilities, not puzzle HUD controls. Local engines are the default; cloud endpoints require explicit consent and protected credentials.");
	}
	if (Query.Contains(TEXT("hint"))
		|| Query.Contains(TEXT("learn"))
		|| Query.Contains(TEXT("practice")))
	{
		return TEXT("Use F1 for a puzzle hint where supported, Tab for guided mode, or return to the Learning Studio from the Puzzle Library. Start with a small exact objective: identify the active axis or target face, make one deliberate move, then verify the resulting state before continuing.");
	}
	return FString::Printf(
		TEXT("You are currently in %s. I can explain its controls, projections, settings, or a structured practice step without an AI provider. Generated responses are off, so no text left this device."),
		*PuzzleTitle);
}

FString UHyperTwistCoachAssistantWidget::BuildRequestUrl(
	const FString& Endpoint
) const
{
	FString Result = Endpoint.TrimStartAndEnd();
	while (Result.EndsWith(TEXT("/")))
	{
		Result.LeftChopInline(1, EAllowShrinking::No);
	}
	if (Result.EndsWith(TEXT("/responses"), ESearchCase::IgnoreCase)
		|| Result.EndsWith(TEXT("/chat/completions"), ESearchCase::IgnoreCase))
	{
		return Result;
	}
	return Result + TEXT("/chat/completions");
}

FString UHyperTwistCoachAssistantWidget::BuildRequestBody(
	const FString& Prompt,
	const bool bUseResponsesApi
) const
{
	const FHyperTwistPlayerPreferences Preferences =
		UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	const FString ContextualInstructions = FString::Printf(
		TEXT("%s\nCurrent runtime: %s. Visible context: %s. Do not claim access to puzzle state that is not stated here."),
		*Preferences.CoachSystemInstructions,
		*PuzzleTitle,
		*PuzzleSubtitle);

	TArray<TSharedPtr<FJsonValue>> Messages;
	HyperTwistCoachAssistantWidgetInternal::AddJsonMessage(
		Messages,
		TEXT("system"),
		ContextualInstructions);
	const int32 HistoryStart = FMath::Max(
		0,
		ConversationMessages.Num()
			- HyperTwistCoachAssistantWidgetInternal::MaximumHistoryMessageCount);
	for (int32 Index = HistoryStart; Index < ConversationMessages.Num(); ++Index)
	{
		if (ConversationRoles.IsValidIndex(Index))
		{
			HyperTwistCoachAssistantWidgetInternal::AddJsonMessage(
				Messages,
				ConversationRoles[Index],
				ConversationMessages[Index]);
		}
	}
	HyperTwistCoachAssistantWidgetInternal::AddJsonMessage(
		Messages,
		TEXT("user"),
		Prompt);

	TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
	Root->SetStringField(TEXT("model"), Preferences.CoachModel);
	Root->SetArrayField(bUseResponsesApi ? TEXT("input") : TEXT("messages"), Messages);
	Root->SetNumberField(TEXT("temperature"), Preferences.CoachTemperature);
	Root->SetNumberField(
		bUseResponsesApi ? TEXT("max_output_tokens") : TEXT("max_tokens"),
		Preferences.CoachMaxResponseTokens);

	FString Json;
	const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Json);
	return FJsonSerializer::Serialize(Root, Writer) ? Json : FString();
}

FString UHyperTwistCoachAssistantWidget::ParseResponseText(
	const FString& ResponseBody,
	const bool bUseResponsesApi,
	FString& OutFailureReason
) const
{
	using namespace HyperTwistCoachAssistantWidgetInternal;
	OutFailureReason.Reset();
	TSharedPtr<FJsonObject> Root;
	const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseBody);
	if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
	{
		OutFailureReason = TEXT("The coach service returned invalid JSON.");
		return FString();
	}
	const FString ProviderError = ExtractErrorMessage(Root);
	if (!ProviderError.IsEmpty())
	{
		OutFailureReason = ProviderError.Left(512);
		return FString();
	}

	FString Text;
	if (bUseResponsesApi)
	{
		Root->TryGetStringField(TEXT("output_text"), Text);
		if (Text.IsEmpty())
		{
			const TArray<TSharedPtr<FJsonValue>>* Outputs = nullptr;
			if (Root->TryGetArrayField(TEXT("output"), Outputs) && Outputs != nullptr)
			{
				for (const TSharedPtr<FJsonValue>& OutputValue : *Outputs)
				{
					const TSharedPtr<FJsonObject>* OutputObject = nullptr;
					if (!OutputValue.IsValid()
						|| !OutputValue->TryGetObject(OutputObject)
						|| OutputObject == nullptr
						|| !OutputObject->IsValid())
					{
						continue;
					}
					const TArray<TSharedPtr<FJsonValue>>* ContentItems = nullptr;
					if (!(*OutputObject)->TryGetArrayField(TEXT("content"), ContentItems)
						|| ContentItems == nullptr)
					{
						continue;
					}
					for (const TSharedPtr<FJsonValue>& ContentValue : *ContentItems)
					{
						const TSharedPtr<FJsonObject>* ContentObject = nullptr;
						FString Candidate;
						if (ContentValue.IsValid()
							&& ContentValue->TryGetObject(ContentObject)
							&& ContentObject != nullptr
							&& ContentObject->IsValid()
							&& (*ContentObject)->TryGetStringField(TEXT("text"), Candidate)
							&& !Candidate.IsEmpty())
						{
							Text += Candidate;
						}
					}
				}
			}
		}
	}
	else
	{
		const TArray<TSharedPtr<FJsonValue>>* Choices = nullptr;
		if (Root->TryGetArrayField(TEXT("choices"), Choices)
			&& Choices != nullptr
			&& Choices->Num() > 0)
		{
			const TSharedPtr<FJsonObject>* ChoiceObject = nullptr;
			const TSharedPtr<FJsonObject>* MessageObject = nullptr;
			if ((*Choices)[0].IsValid()
				&& (*Choices)[0]->TryGetObject(ChoiceObject)
				&& ChoiceObject != nullptr
				&& ChoiceObject->IsValid()
				&& (*ChoiceObject)->TryGetObjectField(TEXT("message"), MessageObject)
				&& MessageObject != nullptr
				&& MessageObject->IsValid())
			{
				(*MessageObject)->TryGetStringField(TEXT("content"), Text);
			}
		}
	}

	Text = Text.TrimStartAndEnd();
	if (Text.IsEmpty())
	{
		OutFailureReason = TEXT("The coach service returned no readable response.");
		return FString();
	}
	return Text.Left(MaximumResponseLength);
}

void UHyperTwistCoachAssistantWidget::HandleSendClicked()
{
	SubmitPrompt(PromptInput != nullptr ? PromptInput->GetText().ToString() : FString());
}

void UHyperTwistCoachAssistantWidget::HandleClearClicked()
{
	if (ConversationList != nullptr)
	{
		ConversationList->ClearChildren();
	}
	ConversationRoles.Reset();
	ConversationMessages.Reset();
	AddConversationMessage(
		TEXT("HYPERTWIST GUIDE"),
		TEXT("Conversation cleared. Ask about controls, this puzzle, settings, or practice."),
		false);
	SetStatus(TEXT("Session-only conversation cleared."));
}

void UHyperTwistCoachAssistantWidget::HandleDictationClicked()
{
	if (AHyperTwistPlayerControllerBase* PlayerController =
			Cast<AHyperTwistPlayerControllerBase>(GetOwningPlayer()))
	{
		PlayerController->ToggleGlobalDictation();
		return;
	}
	SetDictationStatus(TEXT("The global dictation controller is unavailable."), false, true);
}

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

void UHyperTwistCoachAssistantWidget::HandleControlsPromptClicked()
{
	SubmitPrompt(TEXT("Explain the controls for this puzzle."));
}

void UHyperTwistCoachAssistantWidget::HandlePuzzlePromptClicked()
{
	SubmitPrompt(TEXT("Explain this puzzle family and a sensible first practice step."));
}

void UHyperTwistCoachAssistantWidget::HandleSettingsPromptClicked()
{
	SubmitPrompt(TEXT("Where are the control, speech, voice, graphics, and AI settings?"));
}

void UHyperTwistCoachAssistantWidget::HandlePromptCommitted(
	const FText& Text,
	const ETextCommit::Type CommitMethod
)
{
	if (CommitMethod == ETextCommit::OnEnter)
	{
		SubmitPrompt(Text.ToString());
	}
}
