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

#include "InworldRuntime.h"

#include "Graph/InworldData.h"
#include "HAL/PlatformProcess.h"
#include "Interfaces/IPluginManager.h"
#include "InworldRuntimeSettings.h"
#include "InworldRuntimeUtils.h"
#include "Misc/App.h"
#include "Misc/Paths.h"

#if WITH_GAMEPLAY_DEBUGGER_CORE
#include "GameplayDebugger.h"
#endif	  // WITH_GAMEPLAY_DEBUGGER_CORE

#if WITH_GAMEPLAY_DEBUGGER
#include "Debug/GameplayDebuggerCategory_Inworld.h"
#endif	  // WITH_GAMEPLAY_DEBUGGER

THIRD_PARTY_INCLUDES_START
#include "InworldRuntimeLibrary/inworld.h"
THIRD_PARTY_INCLUDES_END

#define LOCTEXT_NAMESPACE "FInworldRuntimeModule"

DEFINE_LOG_CATEGORY(LogInworldRuntime);

static bool SetEnvironmentVariable(const char* Name, const char* Value)
{
	if (!Value || *Value == '\0')
	{
		UE_LOG(LogInworldRuntime, Warning, TEXT("Unset Environment Variable %hs"), Name);
		return false;
	}
	int result = -1;
#if PLATFORM_WINDOWS
	result = _putenv_s(Name, Value);
#elif PLATFORM_LINUX || PLATFORM_MAC
	result = setenv(Name, Value, /*overwrite*/ 1);
#else
	UE_LOG(LogInworldRuntime, Warning, TEXT("Unable to set Environment Variable %hs for platform"), Name);
#endif
	if (result != 0)
	{
		UE_LOG(LogInworldRuntime, Warning, TEXT("Failed to set Environment Variable %hs to %hs"), Name, Value);
		return false;
	}
	return true;
}

static void ExternalLogListener(int severity, const char* message, int length)
{
	switch (severity)
	{
		case 0:	   // Info
			UE_LOG(LogInworldRuntime, Log, TEXT("%hs"), message)
			break;
		case 1:	   // Warning
			UE_LOG(LogInworldRuntime, Warning, TEXT("%hs"), message)
			break;
		case 2:	   // Error
		case 3:	   // Fatal  //NOTE:  We don't want our plugin to crash the engine, so we re-map fatal as error.
			UE_LOG(LogInworldRuntime, Error, TEXT("%hs"), message)
			break;
		default:
			UE_LOG(LogInworldRuntime, Log, TEXT("[Unknown Log Severity %d] %hs"), severity, message)
			break;
	}
}

void FInworldRuntimeModule::StartupModule()
{
	UE_LOG(LogInworldRuntime, Log, TEXT("InworldRuntime v%s Module Startup"), *GetInworldPluginVersion())

#if WITH_GAMEPLAY_DEBUGGER
	IGameplayDebugger& GameplayDebuggerModule = IGameplayDebugger::Get();
	GameplayDebuggerModule.RegisterCategory("InworldGraph",
		IGameplayDebugger::FOnGetCategory::CreateStatic(&FGameplayDebuggerCategory_Inworld::MakeInstance),
		EGameplayDebuggerCategoryState::EnabledInGameAndSimulate);
	GameplayDebuggerModule.NotifyCategoriesChanged();
#endif	  // WITH_GAMEPLAY_DEBUGGER

#if defined(INWORLD_IMPLEMENTATION)

	InitEnvironmentVariables();

	FString AlternativeBinaryFolder{""};
	if (const UInworldRuntimeSettings * Settings{GetDefault<UInworldRuntimeSettings>()};
		Settings != nullptr && !Settings->AlternativeBinaryType.IsEmpty())
	{
		AlternativeBinaryFolder = Settings->AlternativeBinaryType;
	}
	const FString PluginBaseDir{IPluginManager::Get().FindPlugin(TEXT("InworldRuntime"))->GetBaseDir()};
	const FString BinariesSubdirectory{FPlatformProcess::GetBinariesSubdirectory()};
	const FString DllPath{FPaths::Combine(
		PluginBaseDir, "Source", "ThirdParty", "InworldRuntimeLibrary", BinariesSubdirectory, AlternativeBinaryFolder)};

	FPlatformProcess::PushDllDirectory(*DllPath);
#if PLATFORM_WINDOWS
	DllHandle = FPlatformProcess::GetDllHandle(*FPaths::Combine(DllPath, "inworld.dll"));
#elif PLATFORM_LINUX
	DllHandle = FPlatformProcess::GetDllHandle(*FPaths::Combine(DllPath, "libinworld.so"));
#elif PLATFORM_MAC
	DllHandle = FPlatformProcess::GetDllHandle(*FPaths::Combine(DllPath, "libinworld.dylib"));
#endif
	FPlatformProcess::PopDllDirectory(*DllPath);

	if (DllHandle == nullptr)
	{
		UE_LOG(LogInworldRuntime, Error, TEXT("Failed to load inworld shared lib."))
		return;
	}

#if PLATFORM_WINDOWS
	inworld::RegisterExternalLogListener(&ExternalLogListener);
#endif

	if (const inworld::Status status = inworld::InitializeRegistries(); !status.ok())
	{
		UE_LOG(
			LogInworldRuntime, Error, TEXT("Failed to initialize inworld registries: %s"), UTF8_TO_TCHAR(status.ToString().c_str()))
	}

	ConfigureTelemetry();
#endif
}

void FInworldRuntimeModule::ShutdownModule()
{
	UE_LOG(LogInworldRuntime, Log, TEXT("InworldRuntime v%s Module Shutdown"), *GetInworldPluginVersion())

#if WITH_GAMEPLAY_DEBUGGER
	if (IGameplayDebugger::IsAvailable())
	{
		IGameplayDebugger& GameplayDebuggerModule = IGameplayDebugger::Get();
		GameplayDebuggerModule.UnregisterCategory("InworldGraph");
		GameplayDebuggerModule.NotifyCategoriesChanged();
	}
#endif	  // WITH_GAMEPLAY_DEBUGGER

#if defined(INWORLD_IMPLEMENTATION)
	if (DllHandle == nullptr)
	{
		return;
	}

	inworld::ComponentRegistry::GetInstance().Clear();

	if (bTelemetryConfigured)
	{
		inworld::ShutdownTelemetry();
	}

#if PLATFORM_WINDOWS
	inworld::UnregisterExternalLogListener(&ExternalLogListener);
#endif
	FPlatformProcess::FreeDllHandle(DllHandle);
	DllHandle = nullptr;
#endif
}

void FInworldRuntimeModule::InitEnvironmentVariables()
{
	const UInworldRuntimeSettings* Settings{GetDefault<UInworldRuntimeSettings>()};
	if (Settings != nullptr)
	{
		if (!Settings->InworldEndpoint.IsEmpty())
			SetEnvironmentVariable("INWORLD_ENDPOINT", TCHAR_TO_UTF8(*Settings->InworldEndpoint));
		if (!Settings->InworldHttpEndpoint.IsEmpty())
			SetEnvironmentVariable("INWORLD_HTTP_ENDPOINT", TCHAR_TO_UTF8(*Settings->InworldHttpEndpoint));

		if (!Settings->RuntimeApiKey.IsEmpty())
			SetEnvironmentVariable("INWORLD_API_KEY", TCHAR_TO_UTF8(*Settings->RuntimeApiKey));

		int32 verbosity = FMath::Clamp((int32) Settings->TelemetryLogVerbosity, 0, 2);
		SetEnvironmentVariable("VERBOSITY", TCHAR_TO_UTF8(*FString::FromInt(verbosity)));
	}
	SetEnvironmentVariable("INWORLD_EXTRAS_PATH", TCHAR_TO_UTF8(*GetInworldExtrasPath()));
}

void FInworldRuntimeModule::ConfigureTelemetry()
{
	if (bTelemetryConfigured)
	{
		// inworld::ShutdownTelemetry(); - TODO - Once you shut down telemetry it will not be configurable again.
		bTelemetryConfigured = false;
	}

	if (const UInworldRuntimeSettings * Settings{GetDefault<UInworldRuntimeSettings>()};
		Settings != nullptr && !Settings->RuntimeApiKey.IsEmpty())
	{
		if (Settings->bTelemetryEnabled)
		{
			inworld::TracerConfig tracer;
			tracer.enable_span_capture(Settings->bEnableGraphExecutionHistory);

			inworld::LoggerConfig logger;
			logger.sink_absl_logs(true);

			inworld::TelemetryConfig telemetry;
			telemetry.service_name({TCHAR_TO_UTF8(FApp::GetProjectName())});
			telemetry.service_version({TCHAR_TO_UTF8(*GetInworldPluginVersion())});
			telemetry.exporter_type(inworld::ExporterType::kRemote);
			telemetry.endpoint(TCHAR_TO_UTF8(*Settings->InworldEndpoint));
			telemetry.tracer(&tracer);
			telemetry.logger(&logger);
			inworld::ConfigureTelemetry(telemetry);

			bTelemetryConfigured = true;
		}
	}
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FInworldRuntimeModule, InworldRuntime)
