// Copyright (c) 2026 HD Media. MIT licensed - see LICENSE.
//
// find_assets - query the asset registry by class + path + name + tags.
//
// Error format: "find_assets: <error_code>: <human-readable detail>".
// Stable error codes (parseable by clients): missing_params,
// missing_required_field, invalid_class_path, invalid_path_filter,
// invalid_tag_value (v0.7.0).
//
// v0.7.0 additions:
//   - `tags` (object) — TMultiMap<FName, TOptional<FString>> filter; values
//     can be a string (exact-match) or null (any-value / tag-presence).
//   - `include_tags` (bool) — when true, each asset record includes a `tags`
//     map of all its registry tags.

#include "MCP/MCPHandler.h"

#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/AssetData.h"
#include "Modules/ModuleManager.h"
#include "UCMCPCompat.h"

class FHandler_FindAssets : public IUCMCPHandler
{
public:
    virtual FString GetMethodName() const override { return TEXT("find_assets"); }

    virtual TSharedPtr<FJsonObject> Handle(const TSharedPtr<FJsonObject>& Params, FString& OutError) override
    {
        if (!Params.IsValid())
        {
            OutError = TEXT("find_assets: missing_params: request had no params object");
            return nullptr;
        }

        FString ClassPath;
        if (!Params->TryGetStringField(TEXT("class_path"), ClassPath) || ClassPath.IsEmpty())
        {
            OutError = TEXT("find_assets: missing_required_field: 'class_path' is required and must be non-empty");
            return nullptr;
        }

        FString PathUnder = TEXT("/Game/");
        Params->TryGetStringField(TEXT("path_under"), PathUnder);
        if (!PathUnder.StartsWith(TEXT("/Game/")) && !PathUnder.StartsWith(TEXT("/Engine/")))
        {
            OutError = FString::Printf(
                TEXT("find_assets: invalid_path_filter: 'path_under' must start with /Game/ or /Engine/, got '%s'"),
                *PathUnder);
            return nullptr;
        }

        FString NameContains;
        Params->TryGetStringField(TEXT("name_contains"), NameContains);

        int32 Limit = 100;
        Params->TryGetNumberField(TEXT("limit"), Limit);
        Limit = FMath::Clamp(Limit, 1, 500);

        // v0.7.0: optional include_tags toggle
        bool bIncludeTags = false;
        Params->TryGetBoolField(TEXT("include_tags"), bIncludeTags);

        // Build the asset registry filter
        FAssetRegistryModule& Module = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
        IAssetRegistry& Registry = Module.Get();

        // Resolve the class once, up front. If it does not resolve, the
        // class path is invalid -- fail fast: the compat shim needs a
        // UClass*, and a registry query with an unresolvable class is
        // pointless. (This also avoids the version-fragile raw
        // FTopLevelAssetPath/ClassPaths path and the 5.1-only
        // Filter.ClassPaths.Num() post-query guard -- UCMCPCompat is the
        // single cross-version entry point.)
        UClass* FilterClass = LoadClass<UObject>(nullptr, *ClassPath);
        if (!FilterClass)
        {
            OutError = FString::Printf(
                TEXT("find_assets: invalid_class_path: '%s' did not resolve to a UClass"),
                *ClassPath);
            return nullptr;
        }
        FARFilter Filter;
        UCMCPCompat::FilterAddClass(Filter, FilterClass);
        Filter.PackagePaths.Add(FName(*PathUnder));
        Filter.bRecursivePaths = true;

        // v0.7.0: optional tag-based filter. JSON shape:
        //   { "tags": { "TagName": "ExactValue", "OtherTag": null } }
        // null value = any-value (tag-presence test). String value = exact match.
        // Multiple entries are AND-combined by the asset registry.
        const TSharedPtr<FJsonObject>* TagsObjPtr = nullptr;
        if (Params->TryGetObjectField(TEXT("tags"), TagsObjPtr) && TagsObjPtr && (*TagsObjPtr).IsValid())
        {
            for (const auto& Pair : (*TagsObjPtr)->Values)
            {
                const FString& TagName = Pair.Key;
                const TSharedPtr<FJsonValue>& Val = Pair.Value;
                if (!Val.IsValid())
                {
                    continue;
                }
                const FName TagKey(*TagName);
                if (Val->Type == EJson::Null)
                {
                    Filter.TagsAndValues.Add(TagKey, TOptional<FString>());
                }
                else if (Val->Type == EJson::String)
                {
                    Filter.TagsAndValues.Add(TagKey, Val->AsString());
                }
                else
                {
                    OutError = FString::Printf(
                        TEXT("find_assets: invalid_tag_value: tag '%s' must be a string or null, got json type %d"),
                        *TagName, (int32)Val->Type);
                    return nullptr;
                }
            }
        }

        TArray<FAssetData> Found;
        Registry.GetAssets(Filter, Found);
        // (invalid-class is handled fail-fast above; no version-fragile
        // post-query Filter.ClassPaths.Num() guard needed.)

        // Apply the name_contains filter in-memory (case-insensitive)
        TArray<FAssetData> NameFiltered;
        if (!NameContains.IsEmpty())
        {
            for (const FAssetData& Data : Found)
            {
                if (Data.AssetName.ToString().Contains(NameContains, ESearchCase::IgnoreCase))
                {
                    NameFiltered.Add(Data);
                }
            }
        }
        else
        {
            NameFiltered = Found;
        }

        // Sort by asset name for stable output
        NameFiltered.Sort([](const FAssetData& A, const FAssetData& B) {
            return A.AssetName.LexicalLess(B.AssetName);
        });

        const int32 Matched = NameFiltered.Num();
        const int32 Returned = FMath::Min(Matched, Limit);

        TArray<TSharedPtr<FJsonValue>> JsonAssets;
        JsonAssets.Reserve(Returned);
        for (int32 i = 0; i < Returned; ++i)
        {
            const FAssetData& Data = NameFiltered[i];
            TSharedRef<FJsonObject> A = MakeShared<FJsonObject>();
            A->SetStringField(TEXT("name"), Data.AssetName.ToString());
            A->SetStringField(TEXT("package_path"), Data.PackageName.ToString());
            A->SetStringField(TEXT("class"), UCMCPCompat::AssetClassName(Data).ToString());

            // v0.7.0: optionally include all registry tags as a string-keyed map.
            // FAssetTagValueRef::AsString() coerces FName / FString / int32 /
            // object-path to a string representation regardless of original type.
            if (bIncludeTags)
            {
                TSharedRef<FJsonObject> TagsJson = MakeShared<FJsonObject>();
                Data.TagsAndValues.ForEach([&TagsJson](const TPair<FName, FAssetTagValueRef>& TagPair) {
                    TagsJson->SetStringField(TagPair.Key.ToString(), TagPair.Value.AsString());
                });
                A->SetObjectField(TEXT("tags"), TagsJson);
            }

            JsonAssets.Add(MakeShared<FJsonValueObject>(A));
        }

        TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
        Result->SetBoolField(TEXT("ok"), true);
        Result->SetNumberField(TEXT("matched"), Matched);
        Result->SetNumberField(TEXT("returned"), Returned);
        Result->SetArrayField(TEXT("assets"), JsonAssets);
        return Result;
    }
};

TSharedRef<IUCMCPHandler> Make_Handler_FindAssets()
{
    return MakeShared<FHandler_FindAssets>();
}
