// Copyright (c) 2026 HD Media. MIT licensed - see LICENSE.

#include "UnrealClaudeMCPModule.h"
#include "MCP/MCPServer.h"
#include "MCP/MCPHandler.h"
#include "MCP/LogCapture.h"
#include "MCP/EventBus.h"

// Tier 2 event-push delegate sources (PR #40)
#include "Engine/Engine.h"           // UEngine::OnLevelActorAdded/Deleted (Engine.h:2200/2207)
#include "Engine/World.h"            // UWorld::GetPathName for actor.level
#include "GameFramework/Actor.h"     // AActor::GetActorLabel/GetName/GetClass
#include "AssetRegistry/IAssetRegistry.h"        // OnAssetAdded/Removed/Renamed (IAssetRegistry.h:923/930/936, all TS_)
#include "AssetRegistry/AssetRegistryModule.h"   // FAssetRegistryModule loader
#include "AssetRegistry/AssetData.h" // FAssetData fields read by the asset_* payload builders
#include "UCMCPCompat.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"

// Tier 2 event-push delegate sources (PR #41)
#include "Editor.h"                  // FEditorDelegates (PostSaveWorldWithContext / MapChange)
#include "Factories/Factory.h"       // UFactory* param of OnAssetPostImport (Subsystems/ImportSubsystem.h)
#include "Subsystems/ImportSubsystem.h" // UImportSubsystem::OnAssetPostImport (UE 5.7 replacement for deprecated FEditorDelegates::OnAssetPostImport)
// UNVERIFIED-COMPILE (Phase H save-delegate cluster): FObjectPostSaveContext
// and its header UObject/ObjectSaveContext.h are 5.0+. On 4.27 the delegate
// is FEditorDelegates::PostSaveWorld and there is NO ObjectSaveContext.h, so
// guard the include behind the engine gate. The pre-5.0 context type is a
// flagged-UNVERIFIED placeholder (see UCMCP_POST_SAVE_CONTEXT_TYPE in
// UCMCPCompat.h). Source-authored only; no 4.27/5.0 host build this session.
#if UCMCP_ENGINE_AT_LEAST(5, 0)
#include "UObject/ObjectSaveContext.h"  // FObjectPostSaveContext value param of PostSaveWorldWithContext (5.0+)
#endif

DEFINE_LOG_CATEGORY_STATIC(LogUnrealClaudeMCP, Log, All);

// Forward-declared handler factories (one per Handler_*.cpp file in MCP/Handlers/)
extern TSharedRef<IUCMCPHandler> Make_Handler_ExecutePython();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetEngineVersion();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetProjectSummary();
extern TSharedRef<IUCMCPHandler> Make_Handler_ListLevels();
extern TSharedRef<IUCMCPHandler> Make_Handler_SaveDirtyAssets();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetSelectedActors();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectInputMappings();
extern TSharedRef<IUCMCPHandler> Make_Handler_PieControl();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectProjectSetting();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectBlueprint();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectWidgetTree();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectWidgetBlueprint();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectDataTable();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectTexture();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectCurve();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectPhysicsAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectSoundCue();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectSoundWave();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectSoundAttenuation();
extern TSharedRef<IUCMCPHandler> Make_Handler_EditWidgetTree();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetViewportScreenshot();
extern TSharedRef<IUCMCPHandler> Make_Handler_ListTools();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetActorsInLevel();
extern TSharedRef<IUCMCPHandler> Make_Handler_FocusActor();
extern TSharedRef<IUCMCPHandler> Make_Handler_LoadLevel();
extern TSharedRef<IUCMCPHandler> Make_Handler_TakeHighResScreenshot();
extern TSharedRef<IUCMCPHandler> Make_Handler_ImportTexture();
extern TSharedRef<IUCMCPHandler> Make_Handler_ConfigureTexture();
extern TSharedRef<IUCMCPHandler> Make_Handler_FindAssets();
extern TSharedRef<IUCMCPHandler> Make_Handler_SpawnActor();
extern TSharedRef<IUCMCPHandler> Make_Handler_SetActorTransform();
extern TSharedRef<IUCMCPHandler> Make_Handler_DeleteActor();
extern TSharedRef<IUCMCPHandler> Make_Handler_SetActorProperty();
extern TSharedRef<IUCMCPHandler> Make_Handler_AddComponent();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetLogLines();
extern TSharedRef<IUCMCPHandler> Make_Handler_ExecuteConsoleCommand();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_MoveAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_RenameAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_DuplicateAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_DeleteAsset();
extern TSharedRef<IUCMCPHandler> Make_Handler_CreateDmxPatch();
extern TSharedRef<IUCMCPHandler> Make_Handler_DmxStreamSet();
extern TSharedRef<IUCMCPHandler> Make_Handler_DmxStreamStop();
extern TSharedRef<IUCMCPHandler> Make_Handler_DmxStreamStatus();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectSequence();
extern TSharedRef<IUCMCPHandler> Make_Handler_CreateSequence();
extern TSharedRef<IUCMCPHandler> Make_Handler_BindActorToSequence();
extern TSharedRef<IUCMCPHandler> Make_Handler_CreateMaterialInstance();
extern TSharedRef<IUCMCPHandler> Make_Handler_SetMIParameter();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectMaterial();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectMaterialInstance();
extern TSharedRef<IUCMCPHandler> Make_Handler_RunPythonFile();
extern TSharedRef<IUCMCPHandler> Make_Handler_FixUpRedirectors();
extern TSharedRef<IUCMCPHandler> Make_Handler_ApplyPythonToSelection();
extern TSharedRef<IUCMCPHandler> Make_Handler_CompileBlueprint();
extern TSharedRef<IUCMCPHandler> Make_Handler_GetConsoleVariable();
extern TSharedRef<IUCMCPHandler> Make_Handler_SetConsoleVariable();
extern TSharedRef<IUCMCPHandler> Make_Handler_PollEvents();
extern TSharedRef<IUCMCPHandler> Make_Handler_RegisterSubscription();
extern TSharedRef<IUCMCPHandler> Make_Handler_Unsubscribe();
extern TSharedRef<IUCMCPHandler> Make_Handler_PollSubscription();
extern TSharedRef<IUCMCPHandler> Make_Handler_StartSleepTask();
extern TSharedRef<IUCMCPHandler> Make_Handler_PollTask();
extern TSharedRef<IUCMCPHandler> Make_Handler_CancelTask();
extern TSharedRef<IUCMCPHandler> Make_Handler_ListTasks();
extern TSharedRef<IUCMCPHandler> Make_Handler_ExecPythonPersistent();
extern TSharedRef<IUCMCPHandler> Make_Handler_ResetPythonState();
extern TSharedRef<IUCMCPHandler> Make_Handler_RenderCameraToPng();
extern TSharedRef<IUCMCPHandler> Make_Handler_FindConsoleVariables();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectStaticMesh();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectNiagaraSystem();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectAnimBlueprint();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectLandscape();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectSkeletalMesh();
extern TSharedRef<IUCMCPHandler> Make_Handler_InspectAnimMontage();

static constexpr int32 kMCPDefaultPort = 18888;

void FUnrealClaudeMCPModule::StartupModule()
{
    UE_LOG(LogUnrealClaudeMCP, Log, TEXT("[UnrealClaudeMCP] Module started"));

    // Register the log capture device before anything else so early log lines
    // (including handler registration messages) are buffered from the start.
    // CanBeUsedOnAnyThread() = true on the device opts us out of GLog's
    // serializing queue, which would otherwise stall the game thread under
    // heavy startup logging and break the FTSTicker dispatch loop.
    GLog->AddOutputDevice(&FUCMCPLogCapture::Get());

    FUCMCPHandlerRegistry& Reg = FUCMCPHandlerRegistry::Get();
    Reg.Register(Make_Handler_ExecutePython());
    Reg.Register(Make_Handler_GetEngineVersion());
    Reg.Register(Make_Handler_GetProjectSummary());
    Reg.Register(Make_Handler_ListLevels());
    Reg.Register(Make_Handler_SaveDirtyAssets());
    Reg.Register(Make_Handler_GetSelectedActors());
    Reg.Register(Make_Handler_InspectInputMappings());
    Reg.Register(Make_Handler_PieControl());
    Reg.Register(Make_Handler_InspectProjectSetting());
    Reg.Register(Make_Handler_InspectBlueprint());
    Reg.Register(Make_Handler_InspectWidgetTree());
    Reg.Register(Make_Handler_InspectWidgetBlueprint());
    Reg.Register(Make_Handler_InspectDataTable());
    Reg.Register(Make_Handler_InspectTexture());
    Reg.Register(Make_Handler_InspectCurve());
    Reg.Register(Make_Handler_InspectPhysicsAsset());
    Reg.Register(Make_Handler_InspectSoundCue());
    Reg.Register(Make_Handler_InspectSoundWave());
    Reg.Register(Make_Handler_InspectSoundAttenuation());
    Reg.Register(Make_Handler_EditWidgetTree());
    Reg.Register(Make_Handler_GetViewportScreenshot());
    Reg.Register(Make_Handler_ListTools());
    Reg.Register(Make_Handler_GetActorsInLevel());
    Reg.Register(Make_Handler_FocusActor());
    Reg.Register(Make_Handler_LoadLevel());
    Reg.Register(Make_Handler_TakeHighResScreenshot());
    Reg.Register(Make_Handler_ImportTexture());
    Reg.Register(Make_Handler_ConfigureTexture());
    Reg.Register(Make_Handler_FindAssets());
    Reg.Register(Make_Handler_SpawnActor());
    Reg.Register(Make_Handler_SetActorTransform());
    Reg.Register(Make_Handler_DeleteActor());
    Reg.Register(Make_Handler_SetActorProperty());
    Reg.Register(Make_Handler_AddComponent());
    Reg.Register(Make_Handler_GetLogLines());
    Reg.Register(Make_Handler_ExecuteConsoleCommand());
    Reg.Register(Make_Handler_InspectAsset());
    Reg.Register(Make_Handler_MoveAsset());
    Reg.Register(Make_Handler_RenameAsset());
    Reg.Register(Make_Handler_DuplicateAsset());
    Reg.Register(Make_Handler_DeleteAsset());
    Reg.Register(Make_Handler_CreateDmxPatch());
    Reg.Register(Make_Handler_DmxStreamSet());
    Reg.Register(Make_Handler_DmxStreamStop());
    Reg.Register(Make_Handler_DmxStreamStatus());
    Reg.Register(Make_Handler_InspectSequence());
    Reg.Register(Make_Handler_CreateSequence());
    Reg.Register(Make_Handler_BindActorToSequence());
    Reg.Register(Make_Handler_CreateMaterialInstance());
    Reg.Register(Make_Handler_SetMIParameter());
    Reg.Register(Make_Handler_InspectMaterial());
    Reg.Register(Make_Handler_InspectMaterialInstance());
    Reg.Register(Make_Handler_RunPythonFile());
    Reg.Register(Make_Handler_FixUpRedirectors());
    Reg.Register(Make_Handler_ApplyPythonToSelection());
    Reg.Register(Make_Handler_CompileBlueprint());
    Reg.Register(Make_Handler_GetConsoleVariable());
    Reg.Register(Make_Handler_SetConsoleVariable());
    Reg.Register(Make_Handler_PollEvents());
    Reg.Register(Make_Handler_RegisterSubscription());
    Reg.Register(Make_Handler_Unsubscribe());
    Reg.Register(Make_Handler_PollSubscription());
    Reg.Register(Make_Handler_StartSleepTask());
    Reg.Register(Make_Handler_PollTask());
    Reg.Register(Make_Handler_CancelTask());
    Reg.Register(Make_Handler_ListTasks());
    Reg.Register(Make_Handler_ExecPythonPersistent());
    Reg.Register(Make_Handler_ResetPythonState());
    Reg.Register(Make_Handler_RenderCameraToPng());
    Reg.Register(Make_Handler_FindConsoleVariables());
    Reg.Register(Make_Handler_InspectStaticMesh());
    Reg.Register(Make_Handler_InspectNiagaraSystem());
    Reg.Register(Make_Handler_InspectAnimBlueprint());
    Reg.Register(Make_Handler_InspectLandscape());
    Reg.Register(Make_Handler_InspectSkeletalMesh());
    Reg.Register(Make_Handler_InspectAnimMontage());

    // -----------------------------------------------------------------
    // Tier 2 (PR #40): wire 3 starter delegates into the FUCMCPEventBus.
    //
    // Each subscription is a lambda that builds the event-specific JSON
    // payload and calls Bus.Push. The bus is type-agnostic — adding
    // new event sources later means adding more lambdas here, no changes
    // to EventBus.{h,cpp} required.
    //
    // Handles are retained on the module so ShutdownModule can detach
    // before the engine/registry subsystems tear down.
    // -----------------------------------------------------------------

    if (GEngine)
    {
        LevelActorAddedHandle = GEngine->OnLevelActorAdded().AddLambda(
            [](AActor* Actor)
            {
                if (!Actor) { return; }
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("actor_label"), Actor->GetActorLabel());
                Data->SetStringField(TEXT("actor_name"), Actor->GetName());
                Data->SetStringField(TEXT("class"),
                    Actor->GetClass() ? Actor->GetClass()->GetName() : TEXT(""));
                UWorld* World = Actor->GetWorld();
                Data->SetStringField(TEXT("level"),
                    World ? World->GetPathName() : TEXT(""));
                FUCMCPEventBus::Get().Push(TEXT("actor_spawned"), Data);
            });

        LevelActorDeletedHandle = GEngine->OnLevelActorDeleted().AddLambda(
            [](AActor* Actor)
            {
                if (!Actor) { return; }
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("actor_label"), Actor->GetActorLabel());
                Data->SetStringField(TEXT("actor_name"), Actor->GetName());
                Data->SetStringField(TEXT("class"),
                    Actor->GetClass() ? Actor->GetClass()->GetName() : TEXT(""));
                UWorld* World = Actor->GetWorld();
                Data->SetStringField(TEXT("level"),
                    World ? World->GetPathName() : TEXT(""));
                FUCMCPEventBus::Get().Push(TEXT("actor_deleted"), Data);
            });
    }

    {
        // Asset-registry events are TS_ multicasts -- fire from background
        // registry scan threads. Bus locking handles this; nothing special
        // at the call site.
        IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
            TEXT("AssetRegistry")).Get();

        // PR #40: asset_added (initial scan + post-import + in-memory creation).
        AssetAddedHandle = AR.OnAssetAdded().AddLambda(
            [](const FAssetData& AssetData)
            {
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("package_path"), AssetData.PackageName.ToString());
                Data->SetStringField(TEXT("asset_path"), UCMCPCompat::AssetObjectPathString(AssetData));
                Data->SetStringField(TEXT("name"), AssetData.AssetName.ToString());
                Data->SetStringField(TEXT("class"),
                    UCMCPCompat::AssetClassName(AssetData).ToString());
                Data->SetStringField(TEXT("class_path"),
                    AssetData.AssetClassPath.ToString());
                FUCMCPEventBus::Get().Push(TEXT("asset_added"), Data);
            });

        // PR #41: asset_removed -- IAssetRegistry.h:930
        AssetRemovedHandle = AR.OnAssetRemoved().AddLambda(
            [](const FAssetData& AssetData)
            {
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("package_path"), AssetData.PackageName.ToString());
                Data->SetStringField(TEXT("asset_path"), UCMCPCompat::AssetObjectPathString(AssetData));
                Data->SetStringField(TEXT("name"), AssetData.AssetName.ToString());
                Data->SetStringField(TEXT("class"),
                    UCMCPCompat::AssetClassName(AssetData).ToString());
                Data->SetStringField(TEXT("class_path"),
                    AssetData.AssetClassPath.ToString());
                FUCMCPEventBus::Get().Push(TEXT("asset_removed"), Data);
            });

        // PR #41: asset_renamed -- IAssetRegistry.h:936 (two params: new
        // FAssetData + old object path string)
        AssetRenamedHandle = AR.OnAssetRenamed().AddLambda(
            [](const FAssetData& AssetData, const FString& OldObjectPath)
            {
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("new_asset_path"), UCMCPCompat::AssetObjectPathString(AssetData));
                Data->SetStringField(TEXT("old_asset_path"), OldObjectPath);
                Data->SetStringField(TEXT("new_package_path"), AssetData.PackageName.ToString());
                Data->SetStringField(TEXT("name"), AssetData.AssetName.ToString());
                Data->SetStringField(TEXT("class"),
                    UCMCPCompat::AssetClassName(AssetData).ToString());
                Data->SetStringField(TEXT("class_path"),
                    AssetData.AssetClassPath.ToString());
                FUCMCPEventBus::Get().Push(TEXT("asset_renamed"), Data);
            });
    }

    {
        // PR #41: editor delegates (FEditorDelegates is a static-member
        // namespace at Editor.h:184+; subscriptions don't go through any
        // module accessor). These fire on the game thread.

        // asset_post_import -- Subsystems/ImportSubsystem.h. Fires after UFactory
        // finishes importing an asset (whether single import_texture, batch
        // reimport, or drag-and-drop into Content Browser). Distinct from
        // asset_added: asset_added fires for ANY new registry entry (including
        // the initial scan flood); asset_post_import fires only on actual import.
        //
        // UE 5.7 migration (PR closing scorecard follow-up #6): the older
        // FEditorDelegates::OnAssetPostImport static multicast (Editor.h:295/108)
        // is marked C4996 deprecated. The replacement is the editor-subsystem
        // delegate of the same name living on UImportSubsystem. Same callback
        // signature (UFactory*, UObject*), so the lambda body is unchanged --
        // only the attach surface moved off the FEditorDelegates namespace.
        //
        // Module LoadingPhase is PostEngineInit, so GEditor is live by the
        // time StartupModule runs. Guard anyway: a missing subsystem skips
        // the subscription rather than crashing the editor boot.
        if (GEditor)
        {
            if (UImportSubsystem* ImportSubsystem = GEditor->GetEditorSubsystem<UImportSubsystem>())
            {
                AssetPostImportHandle = ImportSubsystem->OnAssetPostImport.AddLambda(
                    [](UFactory* Factory, UObject* Asset)
                    {
                        if (!Asset) { return; }
                        TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                        Data->SetStringField(TEXT("asset_path"), Asset->GetPathName());
                        Data->SetStringField(TEXT("name"), Asset->GetName());
                        Data->SetStringField(TEXT("class"),
                            Asset->GetClass() ? Asset->GetClass()->GetName() : TEXT(""));
                        Data->SetStringField(TEXT("factory"),
                            (Factory && Factory->GetClass()) ? Factory->GetClass()->GetName() : TEXT(""));
                        FUCMCPEventBus::Get().Push(TEXT("asset_post_import"), Data);
                    });
            }
            else
            {
                // Subscription dropped silently leaves callers of poll_events
                // chasing a missing event stream. Surface the failure in the
                // build log instead of going quiet.
                UE_LOG(LogUnrealClaudeMCP, Warning,
                    TEXT("UImportSubsystem unavailable at StartupModule; asset_post_import events will not fire. ")
                    TEXT("Other delegate subscriptions (PostSaveWorldWithContext, MapChange) are unaffected."));
            }
        }
        else
        {
            // PostSaveWorldWithContext and MapChange subscriptions below use the
            // FEditorDelegates static namespace and remain unaffected.
            UE_LOG(LogUnrealClaudeMCP, Warning,
                TEXT("GEditor null at StartupModule despite PostEngineInit phase; ")
                TEXT("asset_post_import subscription skipped."));
        }

        // level_post_save -- Editor.h:273/92. Fires after a UWorld is saved.
        // FObjectPostSaveContext carries cook/save flags; we expose just the
        // world path for now (clients that need cook context can fall back to
        // explicit checks via execute_unreal_python).
        // UNVERIFIED-COMPILE (Phase H save-delegate cluster):
        //   >=5.0 : FEditorDelegates::PostSaveWorldWithContext(UWorld*, FObjectPostSaveContext)
        //   4.27  : FEditorDelegates::PostSaveWorld(uint32 SaveFlags, UWorld* World, bool bSuccess)
        // UCMCP_POST_SAVE_WORLD_DELEGATE selects the delegate name and
        // UCMCP_POST_SAVE_CONTEXT_TYPE the 5.0+ context param. The 4.27
        // PostSaveWorld signature differs in arity/order from the 5.0+ one;
        // its exact parameter list is UNVERIFIED without a 4.27 engine, so the
        // 4.27 branch below is flagged and MUST be confirmed on a real 4.27
        // build before "4.27 supported" can be claimed.
#if UCMCP_ENGINE_AT_LEAST(5, 0)
        PostSaveWorldHandle = FEditorDelegates::UCMCP_POST_SAVE_WORLD_DELEGATE.AddLambda(
            [](UWorld* World, UCMCP_POST_SAVE_CONTEXT_TYPE /*Context*/)
            {
                if (!World) { return; }
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("level"), World->GetPathName());
                FUCMCPEventBus::Get().Push(TEXT("level_post_save"), Data);
            });
#else
        // UNVERIFIED 4.27 -- FEditorDelegates::PostSaveWorld is believed to be
        // (uint32 SaveFlags, UWorld* World, bool bSuccess) on 4.27. Confirm
        // the exact param list on a real 4.27 host build; do NOT trust this
        // lambda shape until then. UCMCP_POST_SAVE_CONTEXT_TYPE is a `bool`
        // placeholder on pre-5.0 (see UCMCPCompat.h) and is intentionally
        // unused here.
        PostSaveWorldHandle = FEditorDelegates::UCMCP_POST_SAVE_WORLD_DELEGATE.AddLambda(
            [](uint32 /*SaveFlags*/, UWorld* World, bool /*bSuccess*/)
            {
                if (!World) { return; }
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetStringField(TEXT("level"), World->GetPathName());
                FUCMCPEventBus::Get().Push(TEXT("level_post_save"), Data);
            });
#endif

        // map_changed -- Editor.h:196/82. Single uint32 flag-bitmap param.
        // Test against the named MapChangeEventFlags constants (Editor.h:435+)
        // rather than literal bit values: stays correct if UE ever renumbers
        // the flags (unlikely but defensive). Emits both the raw int and a
        // humanized flag-name array so callers can filter on either axis
        // without binding to UE's bit values.
        MapChangeHandle = FEditorDelegates::MapChange.AddLambda(
            [](uint32 Flags)
            {
                TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
                Data->SetNumberField(TEXT("flags"), static_cast<double>(Flags));
                TArray<TSharedPtr<FJsonValue>> FlagNames;
                if (Flags & MapChangeEventFlags::NewMap)        { FlagNames.Add(MakeShared<FJsonValueString>(TEXT("new_map"))); }
                if (Flags & MapChangeEventFlags::MapRebuild)    { FlagNames.Add(MakeShared<FJsonValueString>(TEXT("map_rebuild"))); }
                if (Flags & MapChangeEventFlags::WorldTornDown) { FlagNames.Add(MakeShared<FJsonValueString>(TEXT("world_torn_down"))); }
                Data->SetArrayField(TEXT("flag_names"), FlagNames);
                FUCMCPEventBus::Get().Push(TEXT("map_changed"), Data);
            });
    }

    FUCMCPServer::Get().Start(kMCPDefaultPort);
}

void FUnrealClaudeMCPModule::ShutdownModule()
{
    FUCMCPServer::Get().Stop();

    // Detach Tier 2 event-bus subscriptions before the engine subsystems
    // we subscribed to tear down. Guard each removal -- in some shutdown
    // orderings GEngine may already be null even though we're still running.
    if (GEngine)
    {
        if (LevelActorAddedHandle.IsValid())
        {
            GEngine->OnLevelActorAdded().Remove(LevelActorAddedHandle);
            LevelActorAddedHandle.Reset();
        }
        if (LevelActorDeletedHandle.IsValid())
        {
            GEngine->OnLevelActorDeleted().Remove(LevelActorDeletedHandle);
            LevelActorDeletedHandle.Reset();
        }
    }

    // Detach asset-registry subscriptions (PR #40 + PR #41). Use GetModulePtr
    // here -- shutdown is not the time to load modules. If AssetRegistry is
    // already gone we leak the handles, which is harmless (the broadcaster
    // is also gone).
    if (AssetAddedHandle.IsValid()
        || AssetRemovedHandle.IsValid()
        || AssetRenamedHandle.IsValid())
    {
        if (FAssetRegistryModule* ARModule = FModuleManager::GetModulePtr<FAssetRegistryModule>(
                TEXT("AssetRegistry")))
        {
            IAssetRegistry& AR = ARModule->Get();
            if (AssetAddedHandle.IsValid())   { AR.OnAssetAdded().Remove(AssetAddedHandle); }
            if (AssetRemovedHandle.IsValid()) { AR.OnAssetRemoved().Remove(AssetRemovedHandle); }
            if (AssetRenamedHandle.IsValid()) { AR.OnAssetRenamed().Remove(AssetRenamedHandle); }
        }
        AssetAddedHandle.Reset();
        AssetRemovedHandle.Reset();
        AssetRenamedHandle.Reset();
    }

    // Detach editor delegates (PR #41). FEditorDelegates::* are static
    // multicasts; no module-load step required to remove. Safe to call
    // unconditionally once we've checked the handle is valid.
    //
    // OnAssetPostImport: lives on UImportSubsystem (UE 5.7+). Re-fetch the
    // subsystem here so we don't have to cache a pointer that could be stale
    // by ShutdownModule. If GEditor or the subsystem is already torn down,
    // skip the Remove -- the delegate's lifetime is bounded by the subsystem
    // it lives on, so a missing subsystem means the handler is already gone.
    if (AssetPostImportHandle.IsValid())
    {
        if (GEditor)
        {
            if (UImportSubsystem* ImportSubsystem = GEditor->GetEditorSubsystem<UImportSubsystem>())
            {
                ImportSubsystem->OnAssetPostImport.Remove(AssetPostImportHandle);
            }
        }
        AssetPostImportHandle.Reset();
    }
    if (PostSaveWorldHandle.IsValid())
    {
        // UNVERIFIED-COMPILE: UCMCP_POST_SAVE_WORLD_DELEGATE == PostSaveWorldWithContext
        // (>=5.0) or PostSaveWorld (4.27). FDelegateHandle removal is uniform.
        FEditorDelegates::UCMCP_POST_SAVE_WORLD_DELEGATE.Remove(PostSaveWorldHandle);
        PostSaveWorldHandle.Reset();
    }
    if (MapChangeHandle.IsValid())
    {
        FEditorDelegates::MapChange.Remove(MapChangeHandle);
        MapChangeHandle.Reset();
    }

    // Deregister the log capture device before the module unloads so GLog
    // doesn't call into a dangling pointer.
    GLog->RemoveOutputDevice(&FUCMCPLogCapture::Get());

    UE_LOG(LogUnrealClaudeMCP, Log, TEXT("[UnrealClaudeMCP] Module shutdown"));
}

IMPLEMENT_MODULE(FUnrealClaudeMCPModule, UnrealClaudeMCP)
