/**
 * Copyright 2022-2025 Theai, Inc. dba Inworld AI
 */

#include "InworldRuntimeEditor.h"

#include "Assets/AssetActions_InworldTextEmbeddings.h"
#include "Assets/InworldTextEmbeddingAssetDetailsCustomization.h"
#include "EditorUtilitySubsystem.h"
#include "EditorUtilityWidgetBlueprint.h"
#include "Graph/Assets/InworldGraphAsset.h"
#include "Graph/InworldData.h"
#include "Graph/Nodes/InworldNode_Custom.h"
#include "GraphEditor/AssetActions_InworldCustomEdge.h"
#include "GraphEditor/AssetActions_InworldCustomNode.h"
#include "GraphEditor/AssetActions_InworldGraph.h"
#include "GraphEditor/Details/InworldGraphAssetDetailsCustomization.h"
#include "GraphEditor/InworldGraphAssetEditor/EdGraph_InworldGraph.h"
#include "GraphEditor/InworldGraphAssetEditor/InworldGraphEditorStyle.h"
#include "GraphEditor/InworldGraphAssetEditor/InworldGraphPanelNodeFactory.h"
#include "Http.h"
#include "HttpModule.h"
#include "ISettingsModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Interfaces/IPluginManager.h"
#include "InworldRuntime.h"
#include "InworldRuntimeEditorSettings.h"
#include "InworldRuntimeEditorTypes.h"
#include "InworldRuntimeSettings.h"
#include "InworldRuntimeSubsystem.h"
#include "JsonObjectConverter.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "Primitives/TextEmbedder/InworldTextEmbedding.h"
#include "PropertyEditorModule.h"
#include "Style/InworldEditorUIStyle.h"
#include "ToolMenus.h"

#define LOCTEXT_NAMESPACE "FInworldRuntimeEditorModule"

DEFINE_LOG_CATEGORY(LogInworldRuntimeEditor);

static void Inworld_TrackEvent(const FString& Name, const FInworldData_Struct& Event);

void FInworldRuntimeEditorModule::StartupModule()
{
	FInworldEditorUIStyle::Initialize();

	// Register the details panel customization for our text embedding asset
	FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.RegisterCustomClassLayout(UInworldTextEmbeddingAsset::StaticClass()->GetFName(),
		FOnGetDetailCustomizationInstance::CreateStatic(&FInworldTextEmbeddingAssetDetailsCustomization::MakeInstance));
	PropertyModule.RegisterCustomClassLayout(UInworldGraphAsset::StaticClass()->GetFName(),
		FOnGetDetailCustomizationInstance::CreateStatic(&FInworldGraphAssetDetailsCustomization::MakeInstance));

	if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"); SettingsModule != nullptr)
	{
		SettingsModule->RegisterSettings("Project", "Plugins", "Inworld", LOCTEXT("InworldRuntimeSettingsName", "Inworld"),
			LOCTEXT("InworldRuntimeSettingsDescription", "Inworld Runtime Settings"), GetMutableDefault<UInworldRuntimeSettings>());

		SettingsModule->RegisterSettings("Project", "Plugins", "Inworld Editor",
			LOCTEXT("InworldRuntimeEditorSettingsName", "Inworld Editor"),
			LOCTEXT("InworldRuntimeEditorSettingsDescription", "Inworld Runtime Editor Settings"),
			GetMutableDefault<UInworldRuntimeEditorSettings>());
	}

	UToolMenus::RegisterStartupCallback(
		FSimpleMulticastDelegate::FDelegate::CreateStatic(&FInworldRuntimeEditorModule::MenuRegistration));

	GraphEditor_Startup();

	UInworldRuntimeEditorSettings* RuntimeEditorSettings = GetMutableDefault<UInworldRuntimeEditorSettings>();
	if (RuntimeEditorSettings->DistinctId.IsEmpty())
	{
		RuntimeEditorSettings->DistinctId = FString{"Unreal:"} + FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensLower);
		RuntimeEditorSettings->SaveConfig();
		Inworld_TrackEvent(FString{"New User"}, {FInworldWelcomeEvent()});
		if (!GetInworldRuntimeApiKey().IsEmpty())
		{
			Inworld_TrackEvent(FString{"Configured API Key"}, {});
		}
	}
	OnInworldEmitTrackingEventHandle = FInworldRuntimeModule::Get().OnEmitTrackingEvent.AddStatic(&Inworld_TrackEvent);

	OnInworldAssetsPreSaveHandle = FCoreUObjectDelegates::OnObjectPreSave.AddLambda(
		[](UObject* Object, FObjectPreSaveContext SaveContext)
		{
			if (UBlueprint* Blueprint = Cast<UBlueprint>(Object))
			{
				if (Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf(UInworldNode_Custom::StaticClass()))
				{
					UEdGraph_InworldGraph::RefreshOpenedAssets();
				}
			}
		});
}

void FInworldRuntimeEditorModule::MenuRegistration()
{
	UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
	if (!ToolsMenu)
	{
		return;
	}

	const FName SectionName = "Inworld";
	FToolMenuSection* Section = ToolsMenu->FindSection(SectionName);

	// If the section doesn't exist, create it.
	if (!Section)
	{
		UE_LOG(LogInworldRuntimeEditor, Log, TEXT("Cannot find Inworld section, creating one."));
		Section = &ToolsMenu->AddSection(SectionName, LOCTEXT("InworldMenuSection_Label", "Inworld"));
	}

	if (Section)
	{
		Section->AddEntry(FToolMenuEntry::InitMenuEntry("Text Embedding", LOCTEXT("TextEmbeddingMenuItem_Label", "Text Embedding"),
			LOCTEXT("TextEmbeddingMenuItem_ToolTip", "Tool for embedding text records."), FSlateIcon(),
			FExecuteAction::CreateLambda(
				[]()
				{
					UEditorUtilityWidgetBlueprint* EUWBP = LoadObject<UEditorUtilityWidgetBlueprint>(
						nullptr, TEXT("/InworldRuntime/Utilities/EUW_InworldTextEmbedder.EUW_InworldTextEmbedder"));
					UEditorUtilitySubsystem* EditorUtilitySubsystem = GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>();
					if (EditorUtilitySubsystem && EUWBP)
					{
						EditorUtilitySubsystem->SpawnAndRegisterTab(EUWBP);
					}
					else
					{
						UE_LOG(LogInworldRuntimeEditor, Error, TEXT("Failed to spawn Inworld Text Embedder widget."));
					}
				})));
	}
}

void FInworldRuntimeEditorModule::ShutdownModule()
{
	// Unregister the details panel customization
	if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
	{
		FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
		PropertyModule.UnregisterCustomClassLayout(UInworldTextEmbeddingAsset::StaticClass()->GetFName());
		PropertyModule.UnregisterCustomClassLayout(UInworldGraphAsset::StaticClass()->GetFName());
	}

	if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"); SettingsModule != nullptr)
	{
		SettingsModule->UnregisterSettings("Project", "Plugins", "Inworld");
	}

	GraphEditor_Shutdown();

	FInworldRuntimeModule::Get().OnEmitTrackingEvent.Remove(OnInworldEmitTrackingEventHandle);

	FInworldEditorUIStyle::Shutdown();

	if (OnInworldAssetsPreSaveHandle.IsValid())
	{
		FCoreUObjectDelegates::OnObjectPreSave.Remove(OnInworldAssetsPreSaveHandle);
		OnInworldAssetsPreSaveHandle.Reset();
	}
}

void FInworldRuntimeEditorModule::GraphEditor_Startup()
{
	FInworldGraphEditorStyle::Initialize();
	GraphPanelNodeFactory_InworldGraph = MakeShareable(new FGraphPanelNodeFactory_InworldGraph());
	FEdGraphUtilities::RegisterVisualNodeFactory(GraphPanelNodeFactory_InworldGraph);

	IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
	InworldAssetCategoryBit = AssetTools.RegisterAdvancedAssetCategory(
		FName(TEXT("InworldAssetActions")), LOCTEXT("InworldAssetActionsCategory", "Inworld"));

	TSharedRef<IAssetTypeActions> GraphAssetActions = MakeShareable(new FAssetActions_InworldGraph(InworldAssetCategoryBit));
	AssetTools.RegisterAssetTypeActions(GraphAssetActions);
	CreatedAssetTypeActions.Add(GraphAssetActions);

	TSharedRef<IAssetTypeActions> CustomNodeAssetActions =
		MakeShareable(new FAssetActions_InworldCustomNode(InworldAssetCategoryBit));
	AssetTools.RegisterAssetTypeActions(CustomNodeAssetActions);
	CreatedAssetTypeActions.Add(CustomNodeAssetActions);

	TSharedRef<IAssetTypeActions> CustomEdgeAssetActions =
		MakeShareable(new FAssetActions_InworldCustomEdge(InworldAssetCategoryBit));
	AssetTools.RegisterAssetTypeActions(CustomEdgeAssetActions);
	CreatedAssetTypeActions.Add(CustomEdgeAssetActions);

	TSharedRef<IAssetTypeActions> TextEmbeddingsAssetActions =
		MakeShareable(new FAssetActions_InworldTextEmbeddings(InworldAssetCategoryBit));
	AssetTools.RegisterAssetTypeActions(TextEmbeddingsAssetActions);
	CreatedAssetTypeActions.Add(TextEmbeddingsAssetActions);
}

void FInworldRuntimeEditorModule::GraphEditor_Shutdown()
{
	if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
	{
		IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
		for (int32 Index = 0; Index < CreatedAssetTypeActions.Num(); ++Index)
		{
			AssetTools.UnregisterAssetTypeActions(CreatedAssetTypeActions[Index].ToSharedRef());
		}
	}
	if (GraphPanelNodeFactory_InworldGraph.IsValid())
	{
		FEdGraphUtilities::UnregisterVisualNodeFactory(GraphPanelNodeFactory_InworldGraph);
		GraphPanelNodeFactory_InworldGraph.Reset();
	}
	FInworldGraphEditorStyle::Shutdown();
}

static void Inworld_TrackEvent(const FString& Name, const FInworldData_Struct& Event)
{
	TSharedPtr<FJsonObject> EventObject = MakeShareable(new FJsonObject);
	EventObject->SetStringField("event", "[Unreal] " + Name);
	{
		if (TSharedRef<FJsonObject> PropertiesObject = MakeShared<FJsonObject>();
			!Event.IsValid() ||
			FJsonObjectConverter::UStructToJsonObject(Event.GetDataScriptStruct(), Event.GetData(), PropertiesObject))
		{
			for (const FString& ReservedField : {"token", "time", "distinct_id", "$insert_id", "$os"})
			{
				if (PropertiesObject->HasField(ReservedField))
				{
					UE_LOG(LogInworldRuntimeEditor, Warning, TEXT("Event %s already has field '%s'"), *Name, *ReservedField)
				}
			}
			PropertiesObject->SetStringField("token", "2cf13116e9701343ac27ddc452adfbbc");
			PropertiesObject->SetNumberField("time", FDateTime::UtcNow().ToUnixTimestamp());
			const UInworldRuntimeEditorSettings* RuntimeEditorSettings = GetDefault<UInworldRuntimeEditorSettings>();
			PropertiesObject->SetStringField("distinct_id", RuntimeEditorSettings->DistinctId);
			PropertiesObject->SetStringField("$insert_id", FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensLower));
#if PLATFORM_WINDOWS || PLATFORM_MAC || PLATFORM_LINUX
			PropertiesObject->SetStringField("$os",
#if PLATFORM_WINDOWS
				"Windows"
#elif PLATFORM_MAC
				"Mac OS X"
#elif PLATFORM_LINUX
				"Linux"
#endif
			);
#endif
			EventObject->SetObjectField("properties", PropertiesObject);
		}
		else
		{
			UE_LOG(LogInworldRuntimeEditor, Error, TEXT("Event %s is invalid, cannot be tracked."), *Name);
			return;
		}
	}

	TArray<TSharedPtr<FJsonValue>> JsonArray;
	JsonArray.Add(MakeShareable(new FJsonValueObject(EventObject)));

	FString JsonString;
	TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
	FJsonSerializer::Serialize(JsonArray, Writer);

	FHttpRequestRef HttpRequest = FModuleManager::LoadModuleChecked<FHttpModule>("HTTP").CreateRequest();

	HttpRequest->SetURL(TEXT("https://api.mixpanel.com/track?ip=1"));
	HttpRequest->SetHeader(TEXT("accept"), TEXT("text/plain"));
	HttpRequest->SetHeader(TEXT("content-type"), TEXT("application/json"));
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetContentAsString(JsonString);

	HttpRequest->ProcessRequest();
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FInworldRuntimeEditorModule, InworldRuntimeEditor)
