using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using ArchestrA.GRAccess;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;

namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
    internal static class GRAccessCommandDispatcher
    {
        private static readonly string[] KnownScriptBodyFields =
        {
            "ExecuteText",
            "DeclarationsText",
            "StartupText",
            "ShutdownText",
            "OnScanText",
            "OffScanText",
            "Expression"
        };

        public static string Execute(
            IGalaxy galaxy,
            string command,
            string subcommand,
            IDictionary<string, object> args)
        {
            if (galaxy == null) throw new ArgumentNullException(nameof(galaxy));

            command = Normalize(command);
            subcommand = Normalize(subcommand);
            args = args ?? new Dictionary<string, object>();

            if (LlmJson(args) || BoolArg(args, "dry-run"))
                return ExecuteLlm(galaxy, command, subcommand, args);

            return ExecuteCore(galaxy, command, subcommand, args);
        }

        private static string ExecuteLlm(IGalaxy galaxy, string command, string subcommand, IDictionary<string, object> args)
        {
            var commandName = DisplayCommand(command, subcommand);
            var target = Target(args);
            var galaxyName = GalaxyName(galaxy, args);
            try
            {
                var capability = CommandCapabilityRegistry.Find(command, subcommand);
                if (BoolArg(args, "dry-run"))
                {
                    var validation = CommandCapabilityRegistry.Validate(commandName, args, requireMutationConfirmation: true);
                    if (!validation.Valid)
                        return LlmResponse.Ok(commandName, galaxyName, target, new { valid = false, errors = validation.Errors }, unavailable: Array.Empty<LlmUnavailableField>());

                    return LlmResponse.Ok(commandName, galaxyName, target, new { valid = true, dryRun = true, mutationSkipped = capability?.Mutates == true });
                }

                args["json"] = true;
                var output = ExecuteCore(galaxy, command, subcommand, args);
                var data = TryParseJson(output);
                return LlmResponse.Ok(commandName, galaxyName, target, data, unavailable: ExtractUnavailable(data));
            }
            catch (Exception ex)
            {
                return LlmResponse.Fail(commandName, galaxyName, target, ex, 1);
            }
        }

        private static string ExecuteCore(
            IGalaxy galaxy,
            string command,
            string subcommand,
            IDictionary<string, object> args)
        {
            command = Normalize(command);
            subcommand = Normalize(subcommand);

            if (command == "object")
                return ExecuteObject(galaxy, subcommand, args);
            if (command == "area")
                return ExecuteArea(galaxy, subcommand, args);
            if (command == "engine")
                return ExecuteEngine(galaxy, subcommand, args);
            if (command == "io")
                return ExecuteIo(galaxy, subcommand, args);
            if (command == "template")
                return ExecuteTemplate(galaxy, subcommand, args);
            if (command == "instance")
                return ExecuteInstance(galaxy, subcommand, args);
            if (command == "objects")
                return ExecuteObjects(galaxy, subcommand, args);
            if (command == "galaxy")
                return ExecuteGalaxy(galaxy, subcommand, args);
            if (command == "toolset")
                return ExecuteToolset(galaxy, subcommand, args);
            if (command == "script-library")
                return ExecuteScriptLibrary(galaxy, subcommand, args);
            if (command == "security")
                return ExecuteSecurity(galaxy, subcommand, args);
            if (command == "settings")
                return ExecuteSettings(galaxy, subcommand, args);

            throw new NotSupportedException($"Command '{command} {subcommand}' is not implemented.");
        }

        private static string ExecuteObject(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "list":
                    return GRAccessQueryCommandHandler.ExecuteObjectList(galaxy, ToStringArgs(args));
                case "attributes":
                    return GRAccessQueryCommandHandler.ExecuteObjectAttributes(galaxy, ToStringArgs(args));
                case "get":
                    return Format(ObjectDetails(FindSingleObject(galaxy, Kind(args), Arg(args, "name"))), Json(args));
                case "snapshot":
                    return Format(ObjectSnapshot(galaxy, Kind(args), Arg(args, "name")), Json(args));
                case "lineage":
                    return Format(ObjectLineage(galaxy, Kind(args), Arg(args, "name")), Json(args));
                case "children":
                    return Format(ObjectChildren(galaxy, Kind(args), Arg(args, "name")), Json(args));
                case "query-name":
                    return Format(FindObjectsByNames(galaxy, Kind(args), List(args, "name")).Select(ObjectDetails).ToList(), Json(args));
                case "query-condition":
                    return Format(QueryByCondition(galaxy, Kind(args), Arg(args, "condition", "namedLike"), Arg(args, "value", "%")).ToList(), Json(args));
                case "query-multi":
                    return Format(QueryByPatterns(galaxy, Kind(args), List(args, "pattern")).ToList(), Json(args));
                case "extended-attributes":
                    return FormatAttributes(GetExtendedAttributes(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), Arg(args, "attribute", string.Empty), IntArg(args, "level", 0)), Json(args));
                case "help-url":
                    return Scalar(Invoke(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), "GetObjectHelpURL"), Json(args), "helpUrl");
                case "scripts-list":
                case "scripts":
                    return Format(ObjectScripts(galaxy, FindSingleObject(galaxy, Kind(args), Arg(args, "name"))), Json(args));
                case "scripts-get":
                    return Format(ObjectScript(galaxy, FindSingleObject(galaxy, Kind(args), Arg(args, "name")), Arg(args, "script")), Json(args));
                case "scripts-create":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptCreate(galaxy, obj, args));
                case "scripts-delete":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj =>
                    {
                        var scriptName = Arg(args, "script");
                        if (string.IsNullOrWhiteSpace(scriptName))
                            throw new ArgumentException("Script name is required.");
                        obj.DeleteExtensionPrimitive("ScriptExtension", scriptName);
                        return CommandSummary(obj, $"Delete script {scriptName}");
                    });
                case "scripts-set":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptSet(galaxy, obj, Arg(args, "script"), Arg(args, "file"), Arg(args, "field", string.Empty)));
                case "scripts-settings-set":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, "name")), obj => ObjectScriptSettingsSet(obj, args));
                case "checkout":
                case "save":
                case "undo-checkout":
                case "unload":
                    RequireConfirm(args, Arg(args, "name"));
                    return ObjectNoArgOperation(galaxy, args, subcommand);
                case "checkin":
                    RequireConfirm(args, Arg(args, "name"));
                    return ObjectMethod(galaxy, args, "CheckIn", Arg(args, "comment", string.Empty));
                case "set":
                    RequireConfirm(args, Arg(args, "name"));
                    return ObjectSet(galaxy, args);
                default:
                    if (subcommand.StartsWith("attribute-"))
                        return ExecuteAttribute(galaxy, subcommand.Substring("attribute-".Length), args);
                    if (subcommand.StartsWith("uda-"))
                        return ExecuteUda(galaxy, subcommand.Substring("uda-".Length), args);
                    if (subcommand.StartsWith("extension-"))
                        return ExecuteExtension(galaxy, subcommand.Substring("extension-".Length), args);
                    throw new NotSupportedException($"Command 'object {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteTemplate(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "list":
                    return GRAccessQueryCommandHandler.ExecuteTypedList(galaxy, ToStringArgs(args), GRAccessObjectKind.Template);
                case "derive":
                    RequireConfirm(args, Arg(args, "name"));
                    return TemplateMethod(galaxy, args, "CreateTemplate", Arg(args, "new-name"), BoolArg(args, "create-contained"));
                case "instantiate":
                    RequireConfirm(args, Arg(args, "name"));
                    return TemplateMethod(galaxy, args, "CreateInstance", Arg(args, "new-name"), BoolArg(args, "create-contained"));
                case "delete":
                    RequireConfirm(args, Arg(args, "name"));
                    return TemplateMethod(galaxy, args, "DeleteTemplate", EnumValue<EForceDeleteTemplateOption>(Arg(args, "force-option", "dontForceTemplateDelete")));
                default:
                    throw new NotSupportedException($"Command 'template {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteInstance(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "list":
                    return GRAccessQueryCommandHandler.ExecuteTypedList(galaxy, ToStringArgs(args), GRAccessObjectKind.Instance);
                case "delete":
                    RequireConfirm(args, Arg(args, "name"));
                    return InstanceMethod(galaxy, args, "DeleteInstance", EnumValue<EForceDeleteInstanceOption>(Arg(args, "force-option", "undeployIfDeployed")));
                case "deploy":
                    RequireConfirm(args, Arg(args, "name"));
                    return InstanceMethod(galaxy, args, "Deploy", DeployArgs(args));
                case "undeploy":
                    RequireConfirm(args, Arg(args, "name"));
                    return InstanceMethod(galaxy, args, "Undeploy", UndeployArgs(args));
                case "upload":
                    RequireConfirm(args, Arg(args, "name"));
                    return InstanceMethod(galaxy, args, "Upload", UploadArgs(args));
                case "assign-area":
                    RequireConfirm(args, Arg(args, "name"));
                    args["property"] = "area";
                    args["value"] = Arg(args, "area");
                    return ObjectSet(galaxy, args);
                case "assign-engine":
                    RequireConfirm(args, Arg(args, "name"));
                    return AssignEngine(galaxy, args);
                case "assign-container":
                    RequireConfirm(args, Arg(args, "name"));
                    args["property"] = "container";
                    args["value"] = Arg(args, "container");
                    return ObjectSet(galaxy, args);
                default:
                    throw new NotSupportedException($"Command 'instance {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteArea(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "list":
                    return Format(QueryByCondition(galaxy, GRAccessObjectKind.Instance, "namedLike", Arg(args, "pattern", "%Area%")), Json(args));
                case "create":
                    RequireConfirm(args, Arg(args, "template"));
                    var areaName = Arg(args, "name");
                    args["name"] = Arg(args, "template");
                    args["new-name"] = areaName;
                    return TemplateMethod(galaxy, args, "CreateInstance", Arg(args, "new-name"), BoolArg(args, "create-contained"));
                default:
                    throw new NotSupportedException($"Command 'area {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteEngine(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "list":
                    return Format(QueryByCondition(galaxy, GRAccessObjectKind.Instance, "namedLike", Arg(args, "pattern", "%Engine%")), Json(args));
                case "create":
                    RequireConfirm(args, Arg(args, "template"));
                    var engineName = Arg(args, "name");
                    args["name"] = Arg(args, "template");
                    args["new-name"] = engineName;
                    return TemplateMethod(galaxy, args, "CreateInstance", Arg(args, "new-name"), BoolArg(args, "create-contained"));
                default:
                    throw new NotSupportedException($"Command 'engine {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteIo(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "assign":
                    RequireConfirm(args, Arg(args, "name"));
                    var obj = FindSingleObject(galaxy, GRAccessObjectKind.Instance, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var attr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        attr.SetValue(CreateMxValue(Arg(args, "value"), Arg(args, "data-type", "string")));
                        return CommandSummary(attr, "Assign I/O attribute");
                    });
                default:
                    throw new NotSupportedException($"Command 'io {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteObjects(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var targets = ResolveTargetObjects(galaxy, Kind(args), args);
            var expectedTarget = BulkTarget(args);

            switch (subcommand)
            {
                case "checkout":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "CheckOut");
                case "checkin":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "CheckIn", Arg(args, "comment", string.Empty));
                case "undo-checkout":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "UndoCheckOut");
                case "deploy":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "Deploy", DeployArgs(args));
                case "undeploy":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "Undeploy", UndeployArgs(args));
                case "upload":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "Upload", UploadArgs(args));
                case "delete":
                    RequireConfirm(args, expectedTarget);
                    return ObjectsMethod(targets, "DeleteAllObjects");
                case "export":
                    RequireConfirm(args, Arg(args, "output"));
                    targets.ExportObjects(EnumValue<EExportType>(Arg(args, "export-type", "exportAsPDF")), Arg(args, "output"));
                    return CommandSummary(targets, "ExportObjects");
                case "export-protected":
                    RequireConfirm(args, Arg(args, "output"));
                    targets.ExportObjectsAsProtected(Arg(args, "output"));
                    return CommandSummary(targets, "ExportObjectsAsProtected");
                default:
                    throw new NotSupportedException($"Command 'objects {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteGalaxy(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "info":
                    return Format(new
                    {
                        galaxy.Name,
                        galaxy.VersionString,
                        galaxy.VersionNumber,
                        galaxy.UpgradeRequired,
                        galaxy.CdiVersionString
                    }, Json(args));
                case "sync":
                    return GalaxyMethod(galaxy, args, "SynchronizeClient", mutating: false);
                case "cdi-version":
                    return Scalar(galaxy.CdiVersionString, Json(args), "cdiVersion");
                case "defaults-get":
                    return Scalar(galaxy.GetUserDefaults((EUserDefault)EnumValue("EUserDefault", Arg(args, "default"))), Json(args), "value");
                case "defaults-set":
                    RequireConfirm(args, Arg(args, "default"));
                    return GalaxyMethod(galaxy, args, "SetUserDefaults", mutating: true, EnumValue("EUserDefault", Arg(args, "default")), Arg(args, "value"));
                case "backup":
                    RequireConfirm(args, Arg(args, "file"));
                    return GalaxyMethod(galaxy, args, "Backup", true, 0, Arg(args, "file"), Arg(args, "node"), galaxy.Name);
                case "restore":
                    RequireConfirm(args, Arg(args, "file"));
                    return GalaxyMethod(galaxy, args, "Restore", true, 0, Arg(args, "file"), Arg(args, "node"), galaxy.Name, BoolArg(args, "restore-older"));
                case "migrate":
                    RequireConfirm(args, galaxy.Name);
                    return GalaxyMethod(galaxy, args, "MigrateGalaxy", true, galaxy.Name, Arg(args, "node"));
                case "import-objects":
                    RequireConfirm(args, Arg(args, "file"));
                    galaxy.ImportObjects(Arg(args, "file"), BoolArg(args, "overwrite"));
                    return CommandSummary(galaxy, "ImportObjects");
                case "import-objects-ex":
                    RequireConfirm(args, Arg(args, "file"));
                    galaxy.ImportObjectsEx(
                        Arg(args, "file"),
                        EnumValue<E_RESOLVE_VERSION_CONFLICT_ACTION>(Arg(args, "version-conflict")),
                        EnumValue<E_RESOLVE_NAME_CONFLICT_ACTION>(Arg(args, "name-conflict")),
                        Arg(args, "append-name", string.Empty));
                    return CommandSummary(galaxy, "ImportObjectsEx");
                case "export-all":
                    return GalaxyMethod(galaxy, args, "ExportAll", false, EnumValue("EExportType", Arg(args, "export-type", "exportGalaxyDump")), Arg(args, "output"));
                case "grload":
                    RequireConfirm(args, Arg(args, "file"));
                    return GalaxyMethod(galaxy, args, "GRLoad", true, Arg(args, "file"), EnumValue<GRLoadMode>(Arg(args, "mode", "Create")));
                case "import-script-library":
                    RequireConfirm(args, Arg(args, "path"));
                    return GalaxyMethod(galaxy, args, "ImportScriptLib", true, Arg(args, "path"));
                default:
                    throw new NotSupportedException($"Command 'galaxy {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteToolset(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var toolsets = galaxy.QueryToolsets();
            switch (subcommand)
            {
                case "list":
                    return Format(Enumerate(toolsets).Select(o => new { Name = ReadString(o, "Name") }).ToList(), Json(args));
                case "tree":
                    return Format(Enumerate(toolsets).Select(ToolsetTree).ToList(), Json(args));
                case "add":
                    RequireConfirm(args, Arg(args, "name"));
                    Invoke(toolsets, "Add", Arg(args, "name"));
                    return CommandSummary(toolsets, "Add toolset");
                case "delete":
                    RequireConfirm(args, Arg(args, "name"));
                    Invoke(toolsets, "DeleteToolSet", Arg(args, "name"));
                    return CommandSummary(toolsets, "Delete toolset");
                case "rename":
                    RequireConfirm(args, Arg(args, "name"));
                    Invoke(FindIndexed(toolsets, Arg(args, "name")), "Rename", Arg(args, "new-name"));
                    return CommandSummary(FindIndexed(toolsets, Arg(args, "new-name")), "Rename toolset");
                case "move":
                    RequireConfirm(args, Arg(args, "name"));
                    Invoke(FindIndexed(toolsets, Arg(args, "name")), "MoveToToolset", Arg(args, "parent"));
                    return CommandSummary(FindIndexed(toolsets, Arg(args, "name")), "Move toolset");
                default:
                    throw new NotSupportedException($"Command 'toolset {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteScriptLibrary(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var libs = galaxy.QueryScriptLibraries();
            switch (subcommand)
            {
                case "list":
                    return Format(Enumerate(libs).Select(o => new { Name = ReadString(o, "Name") }).ToList(), Json(args));
                case "add":
                case "import":
                    RequireConfirm(args, Arg(args, "path"));
                    Invoke(libs, "Add", Arg(args, "path"));
                    return CommandSummary(libs, "Add script library");
                case "export":
                    RequireConfirm(args, Arg(args, "output"));
                    Invoke(FindIndexed(libs, Arg(args, "name")), "Export", Arg(args, "output"));
                    return CommandSummary(FindIndexed(libs, Arg(args, "name")), "Export script library");
                default:
                    throw new NotSupportedException($"Command 'script-library {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteSecurity(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var security = galaxy.GetReadOnlySecurity();
            switch (subcommand)
            {
                case "info":
                    return Format(new
                    {
                        AuthenticationMode = security.AuthenticationMode.ToString(),
                        LoginTime = security.LoginTime,
                        RoleUpdateInterval = security.RoleUpdateInterval
                    }, Json(args));
                case "roles":
                    return Format(Enumerate(ReadProperty(security, "RolesAvailable")).Select(SecurityRole).ToList(), Json(args));
                case "users":
                    return Format(Enumerate(ReadProperty(security, "UsersAvailable")).Select(SecurityUser).ToList(), Json(args));
                case "groups":
                    return Format(Enumerate(ReadProperty(security, "SecurityGroupsAvailable")).Select(o => new { Name = ReadString(o, "GroupName") }).ToList(), Json(args));
                case "permissions":
                    return Format(Enumerate(ReadProperty(FindIndexed(ReadProperty(security, "RolesAvailable"), Arg(args, "role")), "Permissions")).Select(PermissionInfo).ToList(), Json(args));
                default:
                    throw new NotSupportedException($"Command 'security {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteSettings(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            switch (subcommand)
            {
                case "locale-get":
                    return FormatSettings(galaxy.GetLocaleSettings(), Json(args));
                case "time-master-get":
                    return FormatSettings(galaxy.GetTimeMasterSettings(), Json(args));
                default:
                    throw new NotSupportedException($"Command 'settings {subcommand}' is not implemented.");
            }
        }

        private static string ExecuteAttribute(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            switch (subcommand)
            {
                case "get":
                {
                    var attr = FindAttribute(obj, Arg(args, "attribute"));
                    return Format(AttributeDetails(attr), Json(args));
                }
                case "set":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        editAttr.SetValue(CreateMxValue(Arg(args, "value"), Arg(args, "data-type", "string")));
                        return CommandSummary(editAttr, "Set attribute value");
                    });
                case "value-get":
                {
                    var attr = FindAttribute(obj, Arg(args, "attribute"));
                    return Format(AttributeValueDetails(galaxy, obj, attr), Json(args));
                }
                case "value-set":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        editAttr.SetValue(CreateMxValue(Arg(args, "value"), Arg(args, "data-type", "string")));
                        return CommandSummary(editAttr, "Set attribute value");
                    });
                case "lock":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        editAttr.SetLocked(EnumValue<MxPropertyLockedEnum>(Arg(args, "locked")));
                        return CommandSummary(editAttr, "Set attribute lock");
                    });
                case "security":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        editAttr.SetSecurityClassification(EnumValue<MxSecurityClassification>(Arg(args, "security")));
                        return CommandSummary(editAttr, "Set attribute security");
                    });
                case "buffer":
                    RequireConfirm(args, Arg(args, "name"));
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        var editAttr = FindAttributeForMutation(editObj, Arg(args, "attribute"));
                        editAttr.SetHasBuffer(BoolArg(args, "has-buffer"));
                        return CommandSummary(editAttr, "Set attribute buffer");
                    });
                default:
                    throw new NotSupportedException($"Command 'object attribute-{subcommand}' is not implemented.");
            }
        }

        private static string ExecuteUda(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            RequireConfirm(args, Arg(args, "name"));
            switch (subcommand)
            {
                case "add":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.AddUDA(
                            Arg(args, "uda"),
                            EnumValue<MxDataType>(Arg(args, "data-type")),
                            UdaCategory(Arg(args, "category")),
                            EnumValue<MxSecurityClassification>(Arg(args, "security")),
                            BoolArg(args, "is-array"),
                            IntArg(args, "array-count", 0));
                        return CommandSummary(editObj, "Add UDA");
                    });
                case "delete":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.DeleteUDA(Arg(args, "uda"));
                        return CommandSummary(editObj, "Delete UDA");
                    });
                case "rename":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.RenameUDA(Arg(args, "uda"), Arg(args, "new-name"));
                        return CommandSummary(editObj, "Rename UDA");
                    });
                case "update":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.UpdateUDA(
                            Arg(args, "uda"),
                            EnumValue<MxDataType>(Arg(args, "data-type")),
                            UdaCategory(Arg(args, "category")),
                            EnumValue<MxSecurityClassification>(Arg(args, "security")),
                            BoolArg(args, "is-array"),
                            IntArg(args, "array-count", 0));
                        return CommandSummary(editObj, "Update UDA");
                    });
                default:
                    throw new NotSupportedException($"Command 'object uda-{subcommand}' is not implemented.");
            }
        }

        private static string ExecuteExtension(IGalaxy galaxy, string subcommand, IDictionary<string, object> args)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            RequireConfirm(args, Arg(args, "name"));
            switch (subcommand)
            {
                case "add":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.AddExtensionPrimitive(ExtensionType(Arg(args, "extension-type")), Arg(args, "primitive"), BoolArg(args, "object-extension"));
                        return CommandSummary(editObj, "Add extension primitive");
                    });
                case "delete":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.DeleteExtensionPrimitive(ExtensionType(Arg(args, "extension-type")), Arg(args, "primitive"));
                        return CommandSummary(editObj, "Delete extension primitive");
                    });
                case "rename":
                    return AtomicObjectEdit(obj, editObj =>
                    {
                        editObj.RenameExtensionPrimitive(Arg(args, "primitive"), Arg(args, "new-name"));
                        return CommandSummary(editObj, "Rename extension primitive");
                    });
                default:
                    throw new NotSupportedException($"Command 'object extension-{subcommand}' is not implemented.");
            }
        }

        private static string ObjectNoArgOperation(IGalaxy galaxy, IDictionary<string, object> args, string subcommand)
        {
            var method = subcommand == "checkout"
                ? "CheckOut"
                : subcommand == "undo-checkout"
                    ? "UndoCheckOut"
                    : ToPascal(subcommand);
            return ObjectMethod(galaxy, args, method);
        }

        private static string ObjectSet(IGalaxy galaxy, IDictionary<string, object> args)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            var property = Arg(args, "property");
            var value = Arg(args, "value");
            WriteObjectProperty(galaxy, obj, PropertyName(property), value);
            return CommandSummary(obj, $"Set {property}");
        }

        private static string AtomicObjectEdit(IgObject obj, Func<IgObject, string> mutation)
        {
            var checkedOut = false;

            try
            {
                obj.CheckOut();
                GRAccessDiagnostics.ThrowIfFailed(obj.CommandResult, "CheckOut");
                checkedOut = true;

                var summary = mutation(obj);

                obj.Save();
                GRAccessDiagnostics.ThrowIfFailed(obj.CommandResult, "Save");

                obj.CheckIn(string.Empty);
                checkedOut = false;
                GRAccessDiagnostics.ThrowIfFailed(obj.CommandResult, "CheckIn");

                return summary;
            }
            catch
            {
                if (checkedOut)
                {
                    try
                    {
                        obj.UndoCheckOut();
                    }
                    catch
                    {
                        // Best effort cleanup; preserve the original mutation failure.
                    }
                }

                throw;
            }
        }

        private static string AssignEngine(IGalaxy galaxy, IDictionary<string, object> args)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            var engine = Arg(args, "engine");

            if (IsHostedByEngineThroughArea(galaxy, obj, engine))
                return "Set host: OK";

            args["property"] = "host";
            args["value"] = engine;
            return ObjectSet(galaxy, args);
        }

        private static bool IsHostedByEngineThroughArea(IGalaxy galaxy, IgObject obj, string engine)
        {
            var host = ReadTypedObjectString(obj, "Host");
            if (string.Equals(host, engine, StringComparison.OrdinalIgnoreCase))
                return true;

            var area = ReadTypedObjectString(obj, "Area");
            if (string.IsNullOrWhiteSpace(area))
                return false;

            var areaObject = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, area);
            if (areaObject == null)
                return false;

            return string.Equals(ReadTypedObjectString(areaObject, "Host"), engine, StringComparison.OrdinalIgnoreCase);
        }

        private static string ObjectMethod(IGalaxy galaxy, IDictionary<string, object> args, string method, params object[] parameters)
        {
            var obj = FindSingleObject(galaxy, Kind(args), Arg(args, "name"));
            switch (method)
            {
                case "CheckOut":
                    obj.CheckOut();
                    return CommandSummary(obj, method);
                case "Save":
                    obj.Save();
                    return CommandSummary(obj, method);
                case "Unload":
                    obj.Unload();
                    return CommandSummary(obj, method);
                case "UndoCheckOut":
                    obj.UndoCheckOut();
                    return CommandSummary(obj, method);
                case "CheckIn":
                    obj.CheckIn(parameters.Length > 0 ? parameters[0]?.ToString() : string.Empty);
                    return CommandSummary(obj, method);
            }

            Invoke(obj, method, parameters);
            return CommandSummary(obj, method);
        }

        private static string TemplateMethod(IGalaxy galaxy, IDictionary<string, object> args, string method, params object[] parameters)
        {
            var obj = FindSingleObject(galaxy, GRAccessObjectKind.Template, Arg(args, "name"));
            if (obj is ITemplate template)
            {
                switch (method)
                {
                    case "CreateTemplate":
                        template.CreateTemplate(Convert.ToString(parameters[0], CultureInfo.InvariantCulture), (bool)parameters[1]);
                        return CommandSummary(template, method);
                    case "CreateInstance":
                        template.CreateInstance(Convert.ToString(parameters[0], CultureInfo.InvariantCulture), (bool)parameters[1]);
                        return CommandSummary(template, method);
                    case "DeleteTemplate":
                        template.DeleteTemplate((EForceDeleteTemplateOption)parameters[0]);
                        return CommandSummary(template, method);
                }
            }

            Invoke(obj, method, parameters);
            return CommandSummary(obj, method);
        }

        private static string InstanceMethod(IGalaxy galaxy, IDictionary<string, object> args, string method, params object[] parameters)
        {
            var obj = FindSingleObject(galaxy, GRAccessObjectKind.Instance, Arg(args, "name"));
            if (obj is IInstance instance)
            {
                switch (method)
                {
                    case "Deploy":
                        instance.Deploy(
                            (EActionForCurrentlyDeployedObjects)parameters[0],
                            (ESkipIfCurrentlyUndeployed)parameters[1],
                            (EDeployOnScan)parameters[2],
                            (EForceOffScan)parameters[3],
                            (ECascade)parameters[4],
                            (bool)parameters[5]);
                        return CommandSummary(obj, method);
                    case "Undeploy":
                        instance.Undeploy(
                            (EForceOffScan)parameters[0],
                            (ECascade)parameters[1],
                            (bool)parameters[2]);
                        return CommandSummary(obj, method);
                    case "Upload":
                        instance.Upload(
                            (EAutomaticallyUndocheckout)parameters[0],
                            (ESkipOtherUsersCheckedOutObjects)parameters[1],
                            (ESkipObjectsWithPendingUpdates)parameters[2]);
                        return CommandSummary(obj, method);
                    case "DeleteInstance":
                        instance.DeleteInstance((EForceDeleteInstanceOption)parameters[0]);
                        return CommandSummary(obj, method);
                }
            }

            Invoke(obj, method, parameters);
            return CommandSummary(obj, method);
        }

        private static string ObjectsMethod(object objects, string method, params object[] parameters)
        {
            if (objects is IgObjects typedObjects)
            {
                switch (method)
                {
                    case "CheckOut":
                        typedObjects.CheckOut();
                        return CommandSummary(typedObjects, method);
                    case "CheckIn":
                        typedObjects.CheckIn(parameters.Length > 0 ? parameters[0]?.ToString() : string.Empty);
                        return CommandSummary(typedObjects, method);
                    case "UndoCheckOut":
                        typedObjects.UndoCheckOut();
                        return CommandSummary(typedObjects, method);
                    case "Deploy":
                        typedObjects.Deploy(
                            (EActionForCurrentlyDeployedObjects)parameters[0],
                            (ESkipIfCurrentlyUndeployed)parameters[1],
                            (EDeployOnScan)parameters[2],
                            (EForceOffScan)parameters[3],
                            (bool)parameters[5]);
                        return CommandSummary(typedObjects, method);
                    case "Undeploy":
                        typedObjects.Undeploy(
                            (EForceOffScan)parameters[0],
                            (bool)parameters[2]);
                        return CommandSummary(typedObjects, method);
                    case "Upload":
                        typedObjects.Upload(
                            (EAutomaticallyUndocheckout)parameters[0],
                            (ESkipOtherUsersCheckedOutObjects)parameters[1],
                            (ESkipObjectsWithPendingUpdates)parameters[2]);
                        return CommandSummary(typedObjects, method);
                    case "DeleteAllObjects":
                        typedObjects.DeleteAllObjects();
                        return CommandSummary(typedObjects, method);
                }
            }

            Invoke(objects, method, parameters);
            return CommandSummary(objects, method);
        }

        private static string GalaxyMethod(IGalaxy galaxy, IDictionary<string, object> args, string method, bool mutating, params object[] parameters)
        {
            if (mutating && !BoolArg(args, "confirm"))
                throw new InvalidOperationException("This command requires --confirm.");
            Invoke(galaxy, method, parameters);
            return CommandSummary(galaxy, method);
        }

        private static IReadOnlyList<GRAccessObjectInfo> QueryByCondition(IGalaxy galaxy, GRAccessObjectKind kind, string condition, string value)
        {
            var conditionType = (EConditionType)EnumValue("EConditionType", condition);
            return QueryByConditionValueForms(galaxy, kind, conditionType, value)
                .GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase)
                .Select(g => g.First())
                .ToList();
        }

        private static IReadOnlyList<GRAccessObjectInfo> QueryByConditionValueForms(IGalaxy galaxy, GRAccessObjectKind kind, EConditionType conditionType, string value)
        {
            var result = QueryByConditionRaw(galaxy, kind, conditionType, value).ToList();
            if (result.Any() || !IsRelationshipCondition(conditionType))
                return result;

            foreach (var candidate in ResolveConditionObjects(galaxy, value))
            {
                result = QueryByConditionRaw(galaxy, kind, conditionType, candidate).ToList();
                if (result.Any())
                    return result;

                var tagname = Try(() => candidate.Tagname);
                if (!string.IsNullOrWhiteSpace(tagname))
                {
                    result = QueryByConditionRaw(galaxy, kind, conditionType, tagname).ToList();
                    if (result.Any())
                        return result;
                }
            }

            return result;
        }

        private static IEnumerable<GRAccessObjectInfo> QueryByConditionRaw(IGalaxy galaxy, GRAccessObjectKind kind, EConditionType conditionType, object value)
        {
            if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Template)
                foreach (var obj in QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, "template", conditionType, value))
                    yield return obj;
            if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Instance)
                foreach (var obj in QueryObjects(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, "instance", conditionType, value))
                    yield return obj;
        }

        private static IEnumerable<GRAccessObjectInfo> QueryObjects(IGalaxy galaxy, EgObjectIsTemplateOrInstance templateOrInstance, string kind, EConditionType conditionType, object value)
        {
            var objects = galaxy.QueryObjects(templateOrInstance, conditionType, value, EMatch.MatchCondition);
            GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjects");
            if (objects == null)
                yield break;

            for (var i = 1; i <= objects.count; i++)
            {
                var obj = objects[i];
                if (obj != null)
                    yield return ObjectDetails(obj, kind);
            }
        }

        private static IEnumerable<IgObject> ResolveConditionObjects(IGalaxy galaxy, string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                yield break;

            var template = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, value);
            if (template != null)
                yield return template;

            var instance = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, value);
            if (instance != null)
                yield return instance;
        }

        private static bool IsRelationshipCondition(EConditionType conditionType)
        {
            return conditionType == EConditionType.basedOn
                || conditionType == EConditionType.derivedOrInstantiatedFrom
                || conditionType == EConditionType.containedBy;
        }

        private static IReadOnlyList<GRAccessObjectInfo> QueryByPatterns(IGalaxy galaxy, GRAccessObjectKind kind, IEnumerable<string> patterns)
        {
            var result = new List<GRAccessObjectInfo>();
            foreach (var pattern in patterns.DefaultIfEmpty("%"))
                result.AddRange(QueryByCondition(galaxy, kind, "namedLike", pattern));
            return result.GroupBy(o => $"{o.Kind}:{o.Tagname}").Select(g => g.First()).ToList();
        }

        private static IEnumerable<IgObject> FindObjectsByNames(IGalaxy galaxy, GRAccessObjectKind kind, IEnumerable<string> names)
        {
            foreach (var name in names)
                yield return FindSingleObject(galaxy, kind, name);
        }

        private static IgObject FindSingleObject(IGalaxy galaxy, GRAccessObjectKind kind, string objectName)
        {
            if (string.IsNullOrWhiteSpace(objectName))
                throw new ArgumentException("Object name is required.");

            if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Template)
            {
                var obj = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, objectName);
                if (obj != null) return obj;
            }

            if (kind == GRAccessObjectKind.All || kind == GRAccessObjectKind.Instance)
            {
                var obj = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, objectName);
                if (obj != null) return obj;
            }

            throw new InvalidOperationException($"Object '{objectName}' was not found.");
        }

        private static IgObject FindSingleObjectOrNull(IGalaxy galaxy, EgObjectIsTemplateOrInstance templateOrInstance, string objectName)
        {
            var names = new[] { objectName };
            var objects = galaxy.QueryObjectsByName(templateOrInstance, ref names);
            GRAccessDiagnostics.ThrowIfFailed(galaxy.CommandResult, "QueryObjectsByName");
            return objects == null || objects.count == 0 ? null : objects[1];
        }

        private static IgObjects ResolveTargetObjects(IGalaxy galaxy, GRAccessObjectKind kind, IDictionary<string, object> args)
        {
            var objects = galaxy.CreategObjectCollection();
            var names = List(args, "name").ToList();
            var patterns = List(args, "pattern").ToList();

            foreach (var name in names)
                objects.Add(FindSingleObject(galaxy, kind, name));

            foreach (var obj in patterns.SelectMany(pattern => QueryByCondition(galaxy, kind, "namedLike", pattern)))
                objects.Add(FindSingleObject(galaxy, ParseKind(obj.Kind), obj.Tagname));

            if (!names.Any() && !patterns.Any())
                throw new InvalidOperationException("At least one --name or --pattern target is required.");

            return objects;
        }

        private static IAttribute FindAttribute(IgObject obj, string attributeName)
        {
            if (string.IsNullOrWhiteSpace(attributeName))
                throw new ArgumentException("Attribute name is required.");

            // Prefer ConfigurableAttributes for reads too. The runtime
            // `obj.Attributes` collection sometimes returns an IAttribute proxy
            // for dotted/extension names that resolves `.value` to a synthesized
            // MxValue containing only the parent attribute's prefix (e.g. reading
            // `ProcessRecipe.ExecuteText` returned `"Me.RecipeDownloadFlag"`,
            // truncated at the first `=`). ConfigurableAttributes returns the
            // proper IAttribute backed by the actual ScriptExtension projection.
            return TryFindAttribute(obj.ConfigurableAttributes, attributeName) ?? TryFindAttribute(obj.Attributes, attributeName)
                ?? throw new InvalidOperationException($"Attribute '{attributeName}' was not found.");
        }

        private static IAttribute FindAttributeForMutation(IgObject obj, string attributeName)
        {
            if (string.IsNullOrWhiteSpace(attributeName))
                throw new ArgumentException("Attribute name is required.");

            return TryFindAttribute(obj.ConfigurableAttributes, attributeName) ?? TryFindAttribute(obj.Attributes, attributeName)
                ?? throw new InvalidOperationException($"Attribute '{attributeName}' was not found.");
        }

        private static object TryReadAttributeValue(IAttribute attr)
        {
            if (attr == null) return null;
            try { return attr.value; }
            catch { return null; }
        }

        // Anonymous-type property check that doesn't go through the reflection
        // path used by `ReadProperty` / `ReadString`. The reflection path
        // intermittently returns null/empty on anonymous types from this
        // assembly, which made AttributeValueDetails fall through to the
        // package-binary fallback even when the direct read had succeeded.
        private static bool IsDirectSupported(object direct)
        {
            if (direct == null) return false;
            var prop = direct.GetType().GetProperty("Supported");
            if (prop == null) return false;
            var v = prop.GetValue(direct);
            return v is bool b && b;
        }

        private static string SafeAttributeName(IAttribute attr)
        {
            if (attr == null) return string.Empty;
            try { return attr.Name ?? string.Empty; }
            catch { return string.Empty; }
        }

        private static IAttribute TryFindAttribute(IAttributes attributes, string attributeName)
        {
            if (attributes == null)
                return null;

            try
            {
                return attributes[attributeName];
            }
            catch
            {
                return null;
            }
        }

        private static IEnumerable<GRAccessAttributeInfo> GetExtendedAttributes(IgObject obj, string attribute, int level)
        {
            var categories = new MxAttributeCategory[0];
            IAttributes attrs;
            if (obj is ITemplate template)
                attrs = template.GetExtendedAttributes(attribute, level, categories);
            else if (obj is IInstance instance)
                attrs = instance.GetExtendedAttributes(attribute, level, categories);
            else
                attrs = (IAttributes)Invoke(obj, "GetExtendedAttributes", attribute, level, categories);
            return AttributeInfos(attrs);
        }

        private static IEnumerable<GRAccessAttributeInfo> AttributeInfos(IAttributes attrs)
        {
            if (attrs == null) yield break;
            for (var i = 1; i <= attrs.count; i++)
            {
                var attr = attrs[i];
                if (attr == null) continue;
                yield return AttributeDetails(attr);
            }
        }

        private static GRAccessObjectInfo ObjectDetails(IgObject obj)
        {
            return ObjectDetails(obj, obj is ITemplate ? "template" : "instance");
        }

        private static GRAccessObjectInfo ObjectDetails(IgObject obj, string kind)
        {
            return new GRAccessObjectInfo
            {
                Kind = kind,
                Tagname = Try(() => obj.Tagname),
                ContainedName = Try(() => obj.ContainedName),
                HierarchicalName = Try(() => obj.HierarchicalName),
                CheckoutStatus = Try(() => obj.CheckoutStatus.ToString()),
                DerivedFrom = ReadTypedObjectString(obj, "DerivedFrom"),
                BasedOn = ReadTypedObjectString(obj, "BasedOn"),
                Area = ReadTypedObjectString(obj, "Area"),
                Host = ReadTypedObjectString(obj, "Host"),
                Container = ReadTypedObjectString(obj, "Container"),
                Toolset = ReadObjectName(obj, "Toolset"),
                SecurityGroup = ReadObjectName(obj, "SecurityGroup"),
                ConfigVersion = ReadString(obj, "ConfigVersion"),
                DeploymentStatus = ReadString(obj, "DeploymentStatus"),
                DeployedVersion = ReadString(obj, "DeployedVersion"),
                ValidationErrors = ReadString(obj, "ValidationErrors"),
                ValidationWarnings = ReadString(obj, "ValidationWarnings")
            };
        }

        private static object ObjectSnapshot(IGalaxy galaxy, GRAccessObjectKind kind, string name)
        {
            var unavailable = new List<LlmUnavailableField>();
            var obj = FindSingleObject(galaxy, kind, name);
            var objectInfo = ObjectDetails(obj);
            var package = TryReadPackageSnapshot(galaxy, obj, objectInfo, unavailable);

            var attributes = TryReadList(() => AttributeInfos(obj.Attributes).ToList(), unavailable, "attributes");
            var configurableAttributes = TryReadList(() => AttributeInfos(obj.ConfigurableAttributes).ToList(), unavailable, "configurableAttributes");
            var extendedAttributes = TryReadList(() => GetExtendedAttributes(obj, string.Empty, 0).ToList(), unavailable, "extendedAttributes");
            var scripts = TryReadList(() => ObjectScripts(galaxy, obj, package).ToList(), unavailable, "scripts");
            var lineage = BuildLineage(galaxy, obj, objectInfo, package);
            var children = BuildChildren(galaxy, objectInfo, package).ToList();
            var scriptBodies = MergeScriptBodies(scripts, package).ToList();
            var attributeValues = package.AttributeValues;

            return new
            {
                Object = objectInfo,
                Attributes = attributes,
                ConfigurableAttributes = configurableAttributes,
                ExtendedAttributes = extendedAttributes,
                Relationships = new
                {
                    Area = objectInfo.Area,
                    Host = objectInfo.Host,
                    Container = objectInfo.Container,
                    BasedOn = objectInfo.BasedOn,
                    DerivedFrom = objectInfo.DerivedFrom
                },
                Lineage = lineage,
                Children = children,
                ContainedObjects = children.Where(c => string.Equals(c.Container, objectInfo.Tagname, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(c.HierarchicalName, objectInfo.Tagname + "." + c.ContainedName, StringComparison.OrdinalIgnoreCase)
                    || (!string.IsNullOrWhiteSpace(c.HierarchicalName) && c.HierarchicalName.StartsWith(objectInfo.Tagname + ".", StringComparison.OrdinalIgnoreCase))).ToList(),
                AttributeValues = attributeValues,
                Scripts = scripts,
                ScriptBodies = scriptBodies,
                PackageFallbackUsed = package.PackageFallbackUsed,
                Source = package.PackageFallbackUsed ? "direct-graccess+export-package" : "direct-graccess",
                Unavailable = unavailable
            };
        }

        private static object ObjectLineage(IGalaxy galaxy, GRAccessObjectKind kind, string name)
        {
            var unavailable = new List<LlmUnavailableField>();
            var obj = FindSingleObject(galaxy, kind, name);
            var objectInfo = ObjectDetails(obj);
            var package = TryReadPackageSnapshot(galaxy, obj, objectInfo, unavailable);
            return new
            {
                Object = objectInfo,
                Lineage = BuildLineage(galaxy, obj, objectInfo, package),
                PackageFallbackUsed = package.PackageFallbackUsed,
                Source = package.PackageFallbackUsed ? "direct-graccess+export-package" : "direct-graccess",
                Unavailable = unavailable
            };
        }

        private static object ObjectChildren(IGalaxy galaxy, GRAccessObjectKind kind, string name)
        {
            var unavailable = new List<LlmUnavailableField>();
            var obj = FindSingleObject(galaxy, kind, name);
            var objectInfo = ObjectDetails(obj);
            var package = TryReadPackageSnapshot(galaxy, obj, objectInfo, unavailable);
            var children = BuildChildren(galaxy, objectInfo, package).ToList();
            return new
            {
                Object = objectInfo,
                Children = children,
                ContainedObjects = children.Where(c => string.Equals(c.Container, objectInfo.Tagname, StringComparison.OrdinalIgnoreCase)
                    || (!string.IsNullOrWhiteSpace(c.HierarchicalName) && c.HierarchicalName.StartsWith(objectInfo.Tagname + ".", StringComparison.OrdinalIgnoreCase))).ToList(),
                PackageFallbackUsed = package.PackageFallbackUsed,
                Source = package.PackageFallbackUsed ? "direct-graccess+export-package" : "direct-graccess",
                Unavailable = unavailable
            };
        }

        private static PackageSnapshot TryReadPackageSnapshot(IGalaxy galaxy, IgObject obj, GRAccessObjectInfo objectInfo, List<LlmUnavailableField> unavailable)
        {
            var fallback = new PackageSnapshot();
            string tempDir = null;

            try
            {
                tempDir = Path.Combine(Path.GetTempPath(), "graccess-package-" + Guid.NewGuid().ToString("N"));
                Directory.CreateDirectory(tempDir);

                var output = Path.Combine(tempDir, SafeFileName(objectInfo.Tagname) + ".aaPKG");
                var objects = galaxy.CreategObjectCollection();
                objects.Add(obj);
                objects.ExportObjects(PackageExportType(), output);

                var candidates = Directory.EnumerateFiles(tempDir, "*", SearchOption.AllDirectories).ToList();
                var packagePath = candidates.FirstOrDefault(f => string.Equals(f, output, StringComparison.OrdinalIgnoreCase))
                    ?? candidates.FirstOrDefault(f => string.Equals(Path.GetExtension(f), ".aaPKG", StringComparison.OrdinalIgnoreCase))
                    ?? candidates.FirstOrDefault();

                if (string.IsNullOrWhiteSpace(packagePath) || !File.Exists(packagePath))
                {
                    unavailable.Add(new LlmUnavailableField { Field = "packageFallback", Reason = "GRAccess export completed but no package file was produced." });
                    return fallback;
                }

                var snapshot = PackageSnapshotParser.Parse(packagePath);
                snapshot.PackageFallbackUsed = true;
                snapshot.Source = "export-package";
                unavailable.AddRange(snapshot.Unavailable);
                return snapshot;
            }
            catch (Exception ex)
            {
                unavailable.Add(new LlmUnavailableField { Field = "packageFallback", Reason = "Export package fallback failed: " + RootMessage(ex) });
                return fallback;
            }
            finally
            {
                if (!string.IsNullOrWhiteSpace(tempDir))
                {
                    try { Directory.Delete(tempDir, recursive: true); }
                    catch { }
                }
            }
        }

        private static EExportType PackageExportType()
        {
            return (EExportType)EnumValue("EExportType", "exportAsPDF");
        }

        private static string SafeFileName(string value)
        {
            value = string.IsNullOrWhiteSpace(value) ? "object" : value;
            foreach (var c in Path.GetInvalidFileNameChars())
                value = value.Replace(c, '_');
            return value;
        }

        private static IReadOnlyList<string> BuildLineage(IGalaxy galaxy, IgObject obj, GRAccessObjectInfo objectInfo, PackageSnapshot package)
        {
            if (package.Lineage.Any())
                return AppendIfMissing(package.Lineage, objectInfo.Tagname).ToList();

            var lineage = new List<string>();
            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var current = obj;
            var currentInfo = objectInfo;

            while (currentInfo != null && !string.IsNullOrWhiteSpace(currentInfo.Tagname) && seen.Add(currentInfo.Tagname))
            {
                lineage.Add(currentInfo.Tagname);
                var parentName = FirstNonEmpty(currentInfo.BasedOn, currentInfo.DerivedFrom);
                if (string.IsNullOrWhiteSpace(parentName) || seen.Contains(parentName))
                    break;

                current = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, parentName)
                    ?? FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, parentName);
                if (current == null)
                {
                    lineage.Add(parentName);
                    break;
                }

                currentInfo = ObjectDetails(current);
            }

            lineage.Reverse();
            return lineage;
        }

        private static IEnumerable<GRAccessObjectInfo> BuildChildren(IGalaxy galaxy, GRAccessObjectInfo objectInfo, PackageSnapshot package)
        {
            var results = new List<GRAccessObjectInfo>();
            results.AddRange(package.Children);
            results.AddRange(package.ContainedObjects);

            try
            {
                results.AddRange(QueryByCondition(galaxy, GRAccessObjectKind.All, "namedLike", "%")
                    .Where(candidate => IsChildOf(candidate, objectInfo)));
            }
            catch
            {
                // The package fallback still carries any children it could parse.
            }

            return results
                .Where(o => !string.IsNullOrWhiteSpace(o.Tagname))
                .GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase)
                .Select(g => g.First());
        }

        private static bool IsChildOf(GRAccessObjectInfo candidate, GRAccessObjectInfo parent)
        {
            if (candidate == null || parent == null || string.IsNullOrWhiteSpace(parent.Tagname))
                return false;
            if (string.Equals(candidate.Tagname, parent.Tagname, StringComparison.OrdinalIgnoreCase))
                return false;

            return string.Equals(candidate.DerivedFrom, parent.Tagname, StringComparison.OrdinalIgnoreCase)
                || string.Equals(candidate.BasedOn, parent.Tagname, StringComparison.OrdinalIgnoreCase)
                || string.Equals(candidate.Container, parent.Tagname, StringComparison.OrdinalIgnoreCase)
                || string.Equals(candidate.HierarchicalName, parent.Tagname + "." + candidate.ContainedName, StringComparison.OrdinalIgnoreCase)
                || (!string.IsNullOrWhiteSpace(candidate.HierarchicalName)
                    && candidate.HierarchicalName.StartsWith(parent.Tagname + ".", StringComparison.OrdinalIgnoreCase));
        }

        private static IEnumerable<object> MergeScriptBodies(IEnumerable<object> scripts, PackageSnapshot package)
        {
            var emitted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var script in scripts)
            {
                var name = ReadString(script, "Name");
                var body = FindPackageScriptBody(package, name);
                emitted.Add(name);
                if (body != null)
                {
                    yield return new { Name = name, Body = body.Body, Source = body.Source, BodyAvailable = true };
                }
            }

            foreach (var body in package.ScriptBodies.Where(s => !emitted.Contains(s.Name)))
                yield return new { body.Name, Body = body.Body, Source = body.Source, BodyAvailable = true };
        }

        private static PackageAttributeValue FindPackageAttributeValue(PackageSnapshot package, string attributeName)
        {
            return package.AttributeValues.FirstOrDefault(a => MatchesQualifiedName(a.Name, attributeName));
        }

        private static PackageScriptBody FindPackageScriptBody(PackageSnapshot package, string scriptName)
        {
            return package.ScriptBodies.FirstOrDefault(s => MatchesQualifiedName(s.Name, scriptName));
        }

        private static bool MatchesQualifiedName(string candidate, string requested)
        {
            if (string.IsNullOrWhiteSpace(candidate) || string.IsNullOrWhiteSpace(requested))
                return false;
            if (string.Equals(candidate, requested, StringComparison.OrdinalIgnoreCase))
                return true;
            return candidate.EndsWith("." + requested, StringComparison.OrdinalIgnoreCase)
                || requested.EndsWith("." + candidate, StringComparison.OrdinalIgnoreCase);
        }

        private static IEnumerable<string> AppendIfMissing(IEnumerable<string> values, string value)
        {
            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var item in values.Where(v => !string.IsNullOrWhiteSpace(v)))
            {
                if (seen.Add(item))
                    yield return item;
            }

            if (!string.IsNullOrWhiteSpace(value) && seen.Add(value))
                yield return value;
        }

        private static string FirstNonEmpty(params string[] values)
        {
            return values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? string.Empty;
        }

        private static object AttributeValueDetails(IAttribute attr)
        {
            var value = ReadProperty(attr, "Value");
            return new
            {
                Attribute = AttributeDetails(attr),
                Value = MxValueDetails(value)
            };
        }

        private static object AttributeValueDetails(IGalaxy galaxy, IgObject obj, IAttribute attr)
        {
            var unavailable = new List<LlmUnavailableField>();
            object rawMxValue = TryReadAttributeValue(attr);
            if (rawMxValue == null)
            {
                var name = SafeAttributeName(attr);
                if (!string.IsNullOrEmpty(name))
                {
                    var cfgAttr = TryFindAttribute(obj.ConfigurableAttributes, name);
                    if (cfgAttr != null && !ReferenceEquals(cfgAttr, attr))
                        rawMxValue = TryReadAttributeValue(cfgAttr);
                }
            }
            var direct = MxValueDetails(rawMxValue);
            var package = new PackageSnapshot();
            var packageValue = (PackageAttributeValue)null;

            // Use direct property access to check Supported. Reflecting on the
            // anonymous type via ReadString/ReadProperty was unreliable enough
            // to trigger the package fallback even when MxValueDetails returned
            // Supported=true — the package binary parser then returned a
            // truncated old value, masking the live attribute read.
            if (!IsDirectSupported(direct))
            {
                package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
                packageValue = FindPackageAttributeValue(package, attr.Name);
            }

            if (packageValue != null)
            {
                return new
                {
                    Attribute = AttributeDetails(attr),
                    Value = new
                    {
                        Supported = true,
                        DataType = string.IsNullOrWhiteSpace(packageValue.DataType) ? ReadString(direct, "DataType") : packageValue.DataType,
                        IsArray = false,
                        packageValue.Value,
                        Source = packageValue.Source
                    },
                    PackageFallbackUsed = package.PackageFallbackUsed,
                    Source = "export-package",
                    Unavailable = unavailable
                };
            }

            return new
            {
                Attribute = AttributeDetails(attr),
                Value = direct,
                PackageFallbackUsed = package.PackageFallbackUsed,
                Source = package.PackageFallbackUsed ? "direct-graccess+export-package" : "direct-graccess",
                Unavailable = unavailable
            };
        }

        private static object MxValueDetails(object value)
        {
            if (value == null)
                return new { Supported = false, Value = (object)null, Unavailable = "Attribute value is not exposed by this GRAccess attribute." };

            // The MxValue COM interface is vtable-only — its `GetString` /
            // `GetInteger` / etc. accessors are NOT reachable via IDispatch
            // late-binding. We must cast to the typed `IMxValue` interface.
            // This was the root cause of the earlier "writeback gap" mirage:
            // every read returned "not exposed" because `Invoke(value, "GetString")`
            // threw, never because the underlying value was actually missing.
            var mx = value as IMxValue;
            if (mx == null)
                return new { Supported = false, Value = (object)null, Unavailable = $"Value object is not an IMxValue (got {value.GetType().FullName})." };

            string dataType;
            try { dataType = mx.GetDataType().ToString(); }
            catch { dataType = string.Empty; }

            // Array detection via typed call. `GetDimensionSize` returns 0 for
            // scalars and >0 for arrays (with Dim1 reflecting element count).
            int dimSize = 0;
            try { mx.GetDimensionSize(out dimSize); } catch { }
            var isArray = dimSize > 0;
            if (isArray)
                return new { Supported = false, DataType = dataType, IsArray = true, Value = (object)null, Unavailable = "Array and complex MxValue readback is not yet implemented." };

            // Dispatch on the typed DataType enum. Names mirror the
            // `MxDataType` enum in `ArchestrA.GRAccess.dll`.
            try
            {
                switch (dataType)
                {
                    case "MxBoolean":
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)mx.GetBoolean() };
                    case "MxInteger":
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)mx.GetInteger() };
                    case "MxFloat":
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)mx.GetFloat() };
                    case "MxDouble":
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)mx.GetDouble() };
                    case "MxString":
                    case "MxInternationalizedString":
                    case "MxBigString":
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)mx.GetString() };
                    case "MxTime":
                        // MxTime returns a VBFILETIME via out-param; field
                        // layout differs across interop builds. Skip the
                        // typed read for now; consumers that need timestamps
                        // can fall through to package-fallback reads.
                        return new { Supported = false, DataType = dataType, IsArray = false, Value = (object)null, Unavailable = "MxTime readback is not yet implemented for this dispatcher path." };
                    case "MxElapsedTime":
                    {
                        var e = mx.GetElapsedTime();
                        return new { Supported = true, DataType = dataType, IsArray = false, Value = (object)ElapsedTimeToInt64(e), Unit = "100ns" };
                    }
                }
            }
            catch (Exception ex)
            {
                return new { Supported = false, DataType = dataType, IsArray = false, Value = (object)null, Unavailable = $"Typed accessor for {dataType} threw: {ex.GetType().Name}: {ex.Message}" };
            }

            return new { Supported = false, DataType = dataType, IsArray = false, Value = (object)null, Unavailable = $"No typed accessor for MxDataType {dataType}." };

            return new { Supported = false, DataType = dataType, IsArray = false, Value = (object)null, Unavailable = "No supported scalar accessor succeeded for this MxValue." };
        }

        // Canonical script-text attribute suffixes a `ScriptExtension` primitive
        // projects onto its parent object — see `docs/script-parsing.md` and
        // `docs/usage.md:250`. The presence of any of these as `<prefix>.<suffix>`
        // is the only attribute-side signal that the object carries a real
        // ScriptExtension primitive (the GRAccess COM API does not expose an
        // enumerable for primitives — only `AddExtensionPrimitive` /
        // `DeleteExtensionPrimitive` / `RenameExtensionPrimitive` mutators).
        private static readonly string[] ScriptTextSuffixes =
        {
            "ExecuteText",
            "DeclarationsText",
            "StartupText",
            "ShutdownText",
            "OnScanText",
            "OffScanText",
            "Expression",
        };

        // Returns the ScriptExtension prefix when `name` ends in one of the
        // canonical script-text suffixes (e.g. `Foo.ExecuteText` -> `Foo`),
        // else null.
        private static string ScriptExtensionPrefix(string name)
        {
            if (string.IsNullOrEmpty(name)) return null;
            var dot = name.LastIndexOf('.');
            if (dot <= 0 || dot == name.Length - 1) return null;
            var suffix = name.Substring(dot + 1);
            foreach (var s in ScriptTextSuffixes)
            {
                if (string.Equals(suffix, s, StringComparison.OrdinalIgnoreCase))
                    return name.Substring(0, dot);
            }
            return null;
        }

        private static IEnumerable<object> ObjectScripts(IgObject obj)
        {
            var attrs = AttributeInfos(obj.Attributes).ToList();

            // Group attributes by their ScriptExtension prefix. Each
            // ScriptExtension projects 1..N script-text attributes; one logical
            // script entity per distinct prefix. Drops the previous
            // `IsScriptAttribute` substring heuristic which false-matched
            // `*.Description` (contains "script" in "deSCRIPTion") and
            // `Scan*` attribute UDAs.
            var byPrefix = attrs
                .Select(a => new { Attr = a, Prefix = ScriptExtensionPrefix(a.Name) })
                .Where(x => x.Prefix != null)
                .GroupBy(x => x.Prefix, StringComparer.OrdinalIgnoreCase);

            foreach (var grp in byPrefix)
            {
                var prefix = grp.Key;
                var fields = grp
                    .Select(x => x.Attr.Name.Substring(prefix.Length + 1))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
                    .ToList();

                // Sibling trigger metadata (the ScriptExtension also projects
                // `<prefix>.TriggerType` / `<prefix>.TriggerPeriod` when set).
                var triggerType = attrs.FirstOrDefault(a =>
                    string.Equals(a.Name, prefix + ".TriggerType", StringComparison.OrdinalIgnoreCase))?.DataType;
                var triggerPeriod = attrs.FirstOrDefault(a =>
                    string.Equals(a.Name, prefix + ".TriggerPeriod", StringComparison.OrdinalIgnoreCase))?.DataType;

                yield return new
                {
                    Name = prefix,
                    ExtensionType = "ScriptExtension",
                    Fields = fields,
                    TriggerType = triggerType,
                    TriggerPeriod = triggerPeriod,
                    // Carry empty DataType/Category so the outer ReadString-based
                    // wrapper still works without nulling out the JSON shape.
                    DataType = string.Empty,
                    Category = string.Empty,
                    BodyAvailable = false,
                    Source = "attribute-prefix",
                    Unavailable = "Script body access requires the package fallback path."
                };
            }
        }

        private static IEnumerable<object> ObjectScripts(IGalaxy galaxy, IgObject obj, PackageSnapshot package = null)
        {
            var unavailable = new List<LlmUnavailableField>();
            package = package ?? TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
            var direct = ObjectScripts(obj).ToList();
            var emitted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            foreach (var script in direct)
            {
                var name = ReadString(script, "Name");
                var fields = ReadProperty(script, "Fields") as IReadOnlyList<string>
                    ?? (IReadOnlyList<string>)((ReadProperty(script, "Fields") as IEnumerable<string>)?.ToList());
                emitted.Add(name);
                // Mark every `<prefix>.<field>` as emitted too so the
                // package-only branch below doesn't double-yield each script
                // text field as its own entry.
                if (fields != null)
                {
                    foreach (var f in fields)
                        emitted.Add(name + "." + f);
                }
                // Body lookup: try the prefix first, then the canonical body
                // field `<prefix>.ExecuteText` (the package's serialized
                // script bodies are keyed by full attribute name).
                var body = FindPackageScriptBody(package, name)
                    ?? FindPackageScriptBody(package, name + ".ExecuteText");
                yield return new
                {
                    Name = name,
                    DataType = ReadString(script, "DataType"),
                    Category = ReadString(script, "Category"),
                    ExtensionType = ReadString(script, "ExtensionType"),
                    Fields = fields,
                    TriggerType = ReadString(script, "TriggerType"),
                    TriggerPeriod = ReadString(script, "TriggerPeriod"),
                    BodyAvailable = body != null,
                    Body = body?.Body,
                    Source = body == null ? ReadString(script, "Source") : body.Source,
                    PackageFallbackUsed = package.PackageFallbackUsed,
                    Unavailable = body == null ? ReadString(script, "Unavailable") : string.Empty
                };
            }

            foreach (var body in package.ScriptBodies.Where(s => !emitted.Contains(s.Name)))
            {
                yield return new
                {
                    body.Name,
                    DataType = string.Empty,
                    Category = string.Empty,
                    ExtensionType = "ScriptExtension",
                    Fields = (List<string>)null,
                    TriggerType = (string)null,
                    TriggerPeriod = (string)null,
                    BodyAvailable = true,
                    Body = body.Body,
                    Source = body.Source,
                    PackageFallbackUsed = true,
                    Unavailable = string.Empty
                };
            }
        }

        private static object ObjectScript(IGalaxy galaxy, IgObject obj, string scriptName)
        {
            var unavailable = new List<LlmUnavailableField>();
            var attributeName = ScriptAttributeName(scriptName);

            // Try the live attribute path FIRST. The package-binary parser
            // truncates strings at certain delimiters (e.g. `=`), so when a
            // marker like `// foo` is present in ExecuteText the parser
            // returns only the prefix. The direct GRAccess attribute read via
            // ConfigurableAttributes returns the full string.
            string directBody = null;
            try
            {
                var attr = TryFindAttribute(obj.ConfigurableAttributes, attributeName)
                    ?? TryFindAttribute(obj.Attributes, attributeName);
                if (attr != null)
                {
                    var raw = TryReadAttributeValue(attr);
                    if (raw is IMxValue mx) directBody = mx.GetString();
                }
            }
            catch { directBody = null; }

            var package = TryReadPackageSnapshot(galaxy, obj, ObjectDetails(obj), unavailable);
            var match = ObjectScripts(galaxy, obj, package)
                .FirstOrDefault(s => string.Equals(ReadString(s, "Name"), scriptName, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(ReadString(s, "Name"), attributeName, StringComparison.OrdinalIgnoreCase));
            var packageBody = FindPackageScriptBody(package, attributeName) ?? FindPackageScriptBody(package, scriptName);

            string body = directBody ?? packageBody?.Body;
            string source = directBody != null
                ? "direct-graccess"
                : (packageBody != null ? packageBody.Source : null);

            if (match == null && body == null)
                throw new InvalidOperationException($"Script '{scriptName}' was not found in script-like object attributes.");

            return new
            {
                Metadata = match,
                Body = body,
                BodyAvailable = body != null,
                PackageFallbackUsed = package.PackageFallbackUsed,
                Source = source ?? "direct-graccess",
                Unavailable = body == null
                    ? unavailable.Concat(new[] { new LlmUnavailableField { Field = "scriptBody", Reason = "Script body was not exposed through direct GRAccess and was not found in the exported package." } }).ToList()
                    : unavailable
            };
        }

        private static string ObjectScriptSet(IGalaxy galaxy, IgObject obj, string scriptName, string file, string field = "")
        {
            if (string.IsNullOrWhiteSpace(file))
                throw new ArgumentException("Script source --file is required.");
            if (!File.Exists(file))
                throw new FileNotFoundException("Script source file was not found.", file);

            string attributeName;
            if (!string.IsNullOrWhiteSpace(field))
            {
                // Validate against the canonical script-text suffix list so
                // callers get a friendly error before we try to write a
                // nonexistent attribute.
                if (!ScriptTextSuffixes.Any(s => string.Equals(s, field, StringComparison.OrdinalIgnoreCase)))
                    throw new ArgumentException($"--field must be one of: {string.Join(", ", ScriptTextSuffixes)}");
                attributeName = scriptName + "." + field;
            }
            else
            {
                attributeName = ScriptAttributeName(scriptName);
            }

            var attr = FindAttributeForMutation(obj, attributeName);
            var body = File.ReadAllText(file);
            attr.SetValue(CreateMxValue(body, "string"));
            return CommandSummary(attr, $"Set script {attributeName}");
        }

        private static string ObjectScriptCreate(IGalaxy galaxy, IgObject obj, IDictionary<string, object> args)
        {
            var scriptName = Arg(args, "script");
            if (string.IsNullOrWhiteSpace(scriptName))
                throw new ArgumentException("Script name is required.");

            obj.AddExtensionPrimitive("ScriptExtension", scriptName, true);
            var summaries = new List<string> { CommandSummary(obj, $"Create script {scriptName}") };

            var file = Arg(args, "file", string.Empty);
            if (!string.IsNullOrWhiteSpace(file))
                summaries.Add(ObjectScriptSet(galaxy, obj, scriptName, file));

            summaries.AddRange(SetScriptSettings(obj, args));
            return string.Join(Environment.NewLine, summaries.Where(s => !string.IsNullOrWhiteSpace(s)));
        }

        private static string ObjectScriptSettingsSet(IgObject obj, IDictionary<string, object> args)
        {
            var summaries = SetScriptSettings(obj, args).ToList();
            if (summaries.Count == 0)
                throw new InvalidOperationException("At least one script setting option is required.");

            return string.Join(Environment.NewLine, summaries);
        }

        private static IEnumerable<string> SetScriptSettings(IgObject obj, IDictionary<string, object> args)
        {
            var scriptName = Arg(args, "script");
            if (string.IsNullOrWhiteSpace(scriptName))
                throw new ArgumentException("Script name is required.");

            var triggerPeriodMs = OptionalArg(args, "trigger-period-ms");
            if (!string.IsNullOrWhiteSpace(triggerPeriodMs))
            {
                var attr = FindAttributeForMutation(obj, scriptName + ".TriggerPeriod");
                attr.SetValue(CreateMxValue(triggerPeriodMs, "elapsed-ms"));
                yield return CommandSummary(attr, $"Set script {scriptName}.TriggerPeriod");

                if (BoolArg(args, "lock-trigger-period"))
                {
                    attr.SetLocked(MxPropertyLockedEnum.MxLockedInMe);
                    yield return CommandSummary(attr, $"Lock script {scriptName}.TriggerPeriod");
                }
            }

            var triggerType = OptionalArg(args, "trigger-type");
            if (!string.IsNullOrWhiteSpace(triggerType))
            {
                var attr = FindAttributeForMutation(obj, scriptName + ".TriggerType");
                attr.SetValue(CreateMxValue(triggerType, "string"));
                yield return CommandSummary(attr, $"Set script {scriptName}.TriggerType");

                if (BoolArg(args, "lock-trigger-type"))
                {
                    attr.SetLocked(MxPropertyLockedEnum.MxLockedInMe);
                    yield return CommandSummary(attr, $"Lock script {scriptName}.TriggerType");
                }
            }

            var expression = OptionalArg(args, "expression");
            if (!string.IsNullOrWhiteSpace(expression))
            {
                var attr = FindAttributeForMutation(obj, scriptName + ".Expression");
                attr.SetValue(CreateMxValue(expression, "string"));
                yield return CommandSummary(attr, $"Set script {scriptName}.Expression");
            }
        }

        private static string ScriptAttributeName(string scriptName)
        {
            if (string.IsNullOrWhiteSpace(scriptName))
                throw new ArgumentException("Script name is required.");

            var trimmed = scriptName.Trim();
            if (KnownScriptBodyFields.Any(field => trimmed.EndsWith("." + field, StringComparison.OrdinalIgnoreCase)))
                return trimmed;
            if (trimmed.EndsWith("_ScriptExtension", StringComparison.OrdinalIgnoreCase))
                return trimmed.Substring(0, trimmed.Length - "_ScriptExtension".Length) + ".ExecuteText";
            return trimmed + ".ExecuteText";
        }

        private static GRAccessAttributeInfo AttributeDetails(IAttribute attr)
        {
            return new GRAccessAttributeInfo
            {
                Name = Try(() => attr.Name),
                DataType = Try(() => attr.DataType.ToString()),
                Category = Try(() => attr.AttributeCategory.ToString()),
                SecurityClassification = Try(() => attr.SecurityClassification.ToString()),
                Locked = Try(() => attr.Locked.ToString()),
                UpperBoundDim1 = TryNullable(() => attr.UpperBoundDim1),
                HasBuffer = TryNullable(() => attr.HasBuffer),
                RuntimeSetHandler = TryNullable(() => attr.RtSethandler),
                ConfigSetHandler = TryNullable(() => attr.CfgSethandler)
            };
        }

        private static object ToolsetTree(object toolset)
        {
            return new
            {
                Name = ReadString(toolset, "Name"),
                Children = Enumerate(Invoke(toolset, "GetChildToolsets")).Select(ToolsetTree).ToList()
            };
        }

        private static object SecurityRole(object role)
        {
            return new
            {
                RoleName = ReadString(role, "RoleName"),
                AccessLevel = ReadString(role, "AccessLevel")
            };
        }

        private static object SecurityUser(object user)
        {
            return new
            {
                UserName = ReadString(user, "UserName"),
                FullName = ReadString(user, "FullName")
            };
        }

        private static object PermissionInfo(object permission)
        {
            return new
            {
                PermissionName = ReadString(permission, "PermissionName"),
                PermissionParentName = ReadString(permission, "PermissionParentName"),
                IsConfigured = ReadString(permission, "IsConfigured"),
                HasChildren = ReadString(permission, "HasChildren"),
                IsSecurityGroup = ReadString(permission, "IsSecurityGroup"),
                SecurityGroup = ReadString(permission, "SecurityGroup")
            };
        }

        private static string FormatSettings(object settings, bool json)
        {
            return Format(new
            {
                Instance = ReadString(ReadProperty(settings, "Instance"), "Tagname")
            }, json);
        }

        private static string FormatAttributes(IEnumerable<GRAccessAttributeInfo> attributes, bool json)
        {
            var list = attributes.ToList();
            if (json)
                return JsonConvert.SerializeObject(list, Formatting.Indented);
            return string.Join(Environment.NewLine, list.Select(a => $"{a.Name}\t{a.DataType}\t{a.Category}"));
        }

        private static string Format(object value, bool json)
        {
            if (json)
                return JsonConvert.SerializeObject(value, Formatting.Indented);

            if (value is IEnumerable enumerable && !(value is string))
            {
                var lines = new List<string>();
                foreach (var item in enumerable)
                    lines.Add(Flatten(item));
                return string.Join(Environment.NewLine, lines);
            }

            return Flatten(value);
        }

        private static string Scalar(object value, bool json, string name)
        {
            return json ? JsonConvert.SerializeObject(new Dictionary<string, object> { [name] = value }, Formatting.Indented) : Convert.ToString(value, CultureInfo.InvariantCulture);
        }

        private static object TryParseJson(string output)
        {
            if (string.IsNullOrWhiteSpace(output))
                return null;

            try
            {
                return JToken.Parse(output);
            }
            catch
            {
                return output;
            }
        }

        private static IEnumerable<LlmUnavailableField> ExtractUnavailable(object data)
        {
            if (data is JObject obj)
            {
                var token = obj["unavailable"] ?? obj["Unavailable"];
                if (token is JArray array)
                {
                    var fields = array.ToObject<List<LlmUnavailableField>>();
                    return fields != null ? (IEnumerable<LlmUnavailableField>)fields : Array.Empty<LlmUnavailableField>();
                }
            }

            return Array.Empty<LlmUnavailableField>();
        }

        private static string DisplayCommand(string command, string subcommand)
        {
            var normalized = $"{Normalize(command)} {Normalize(subcommand).Replace('-', ' ')}".Trim();
            return string.Join(" ", normalized.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
        }

        private static string Target(IDictionary<string, object> args)
        {
            foreach (var key in new[] { "name", "output", "file", "path", "template", "galaxy", "pattern", "default" })
            {
                if (args != null && args.TryGetValue(key, out var value) && value != null)
                    return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
            }

            return string.Empty;
        }

        private static string GalaxyName(IGalaxy galaxy, IDictionary<string, object> args)
        {
            if (args != null && args.TryGetValue("galaxy", out var value) && value != null)
                return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;

            return Try(() => galaxy.Name);
        }

        private static string Flatten(object value)
        {
            if (value == null) return string.Empty;
            if (value is string s) return s;

            var properties = value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            if (properties.Length == 0)
                return Convert.ToString(value, CultureInfo.InvariantCulture);

            return string.Join("\t", properties.Select(p => Convert.ToString(p.GetValue(value), CultureInfo.InvariantCulture)));
        }

        private static string CommandSummary(object target, string operation)
        {
            if (target is IAttribute attribute)
                return CommandResultSummary(attribute.CommandResult, operation);

            if (target is IgObject gObject)
                return CommandResultSummary(gObject.CommandResult, operation);

            if (target is IgObjects gObjects)
                return CommandResultsSummary(gObjects.CommandResults, operation);

            if (target is IGalaxy galaxy)
            {
                if (galaxy.CommandResult != null)
                    return CommandResultSummary(galaxy.CommandResult, operation);

                if (galaxy.CommandResults != null)
                    return CommandResultsSummary(galaxy.CommandResults, operation);
            }

            var result = ReadProperty(target, "CommandResult");
            if (result != null)
            {
                return CommandResultSummary((ICommandResult)result, operation);
            }

            var results = ReadProperty(target, "CommandResults");
            if (results != null)
            {
                var completelySuccessful = ReadString(results, "CompletelySuccessful");
                if (string.Equals(completelySuccessful, "False", StringComparison.OrdinalIgnoreCase))
                    throw new InvalidOperationException($"{operation} failed: CommandResults.CompletelySuccessful=False");

                return $"{operation}: {completelySuccessful}";
            }

            return $"{operation}: OK";
        }

        private static string CommandResultSummary(ICommandResult result, string operation)
        {
            if (result == null)
                return $"{operation}: OK";

            GRAccessDiagnostics.ThrowIfFailed(result, operation);
            return GRAccessDiagnostics.FormatCommandResult(operation, result);
        }

        private static string CommandResultsSummary(ICommandResults results, string operation)
        {
            if (results == null)
                return $"{operation}: OK";

            if (!results.CompletelySuccessful)
                throw new InvalidOperationException($"{operation} failed: CommandResults.CompletelySuccessful=False");

            return $"{operation}: {results.CompletelySuccessful}";
        }

        // COM IDispatch is case-insensitive at runtime, but .NET's `InvokeMember`
        // is not unless `BindingFlags.IgnoreCase` is set. Some IAttribute proxies
        // expose properties under a lowercase name (e.g. `value` on `IAttribute`)
        // while siblings expose CamelCase (e.g. `DataType` on `IMxValue`). Without
        // IgnoreCase a `ReadProperty(attr, "Value")` lookup silently returns null
        // for one and works for the other — the gap that masked all script-body
        // writes as "no-ops" in earlier versions of `MxValueDetails`.
        private static object Invoke(object target, string method, params object[] parameters)
        {
            return target.GetType().InvokeMember(
                method,
                BindingFlags.InvokeMethod | BindingFlags.IgnoreCase,
                null,
                target,
                parameters);
        }

        private static object ReadProperty(object target, string property)
        {
            if (target == null || string.IsNullOrWhiteSpace(property)) return null;
            try
            {
                return target.GetType().InvokeMember(
                    property,
                    BindingFlags.GetProperty | BindingFlags.IgnoreCase,
                    null,
                    target,
                    null);
            }
            catch
            {
                return null;
            }
        }

        private static string ReadObjectName(object target, string property)
        {
            var value = ReadProperty(target, property);
            if (value == null)
                return string.Empty;

            var tagname = ReadString(value, "Tagname");
            if (!string.IsNullOrWhiteSpace(tagname))
                return tagname;

            var name = ReadString(value, "Name");
            if (!string.IsNullOrWhiteSpace(name))
                return name;

            return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
        }

        private static string ReadTypedObjectString(IgObject target, string property)
        {
            object value = null;
            if (target is ITemplate template)
                value = ReadInterfaceProperty(typeof(ITemplate), template, property);
            if (value == null && target is IInstance instance)
                value = ReadInterfaceProperty(typeof(IInstance), instance, property);
            if (value == null)
                return ReadObjectName(target, property);

            var tagname = ReadString(value, "Tagname");
            if (!string.IsNullOrWhiteSpace(tagname))
                return tagname;

            var name = ReadString(value, "Name");
            if (!string.IsNullOrWhiteSpace(name))
                return name;

            return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
        }

        private static object ReadInterfaceProperty(Type interfaceType, object target, string property)
        {
            try
            {
                var propertyInfo = interfaceType.GetProperty(property);
                if (propertyInfo != null)
                    return propertyInfo.GetValue(target, null);
            }
            catch
            {
                // Fall back to late-bound COM reflection below.
            }

            try
            {
                return interfaceType.InvokeMember(property, BindingFlags.GetProperty, null, target, null);
            }
            catch
            {
                return null;
            }
        }

        private static void WriteProperty(object target, string property, object value)
        {
            target.GetType().InvokeMember(property, BindingFlags.SetProperty, null, target, new[] { value });
        }

        private static void WriteObjectProperty(IGalaxy galaxy, IgObject target, string property, string value)
        {
            if (WriteTypedObjectProperty(target, property, value))
                return;

            if (IsObjectReferenceProperty(property))
            {
                var referenced = FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsInstance, value)
                    ?? FindSingleObjectOrNull(galaxy, EgObjectIsTemplateOrInstance.gObjectIsTemplate, value);
                if (referenced != null)
                {
                    try
                    {
                        WriteProperty(target, property, referenced);
                        return;
                    }
                    catch
                    {
                        // Fall back to the raw value below. Some GRAccess properties accept names directly.
                    }
                }
            }

            WriteProperty(target, property, value);
        }

        private static bool WriteTypedObjectProperty(IgObject target, string property, string value)
        {
            if (target is IInstance instance)
            {
                switch (property)
                {
                    case "Tagname": instance.Tagname = value; return true;
                    case "ContainedName": instance.ContainedName = value; return true;
                    case "Area": instance.Area = value; return true;
                    case "Host": instance.Host = value; return true;
                    case "Container": instance.Container = value; return true;
                }
            }

            if (target is ITemplate template)
            {
                switch (property)
                {
                    case "Tagname": template.Tagname = value; return true;
                    case "ContainedName": template.ContainedName = value; return true;
                    case "Area": template.Area = value; return true;
                    case "Host": template.Host = value; return true;
                    case "Container": template.Container = value; return true;
                    case "Toolset": template.Toolset = value; return true;
                }
            }

            return false;
        }

        private static bool IsObjectReferenceProperty(string property)
        {
            return string.Equals(property, "Area", StringComparison.OrdinalIgnoreCase)
                || string.Equals(property, "Host", StringComparison.OrdinalIgnoreCase)
                || string.Equals(property, "Container", StringComparison.OrdinalIgnoreCase)
                || string.Equals(property, "Toolset", StringComparison.OrdinalIgnoreCase)
                || string.Equals(property, "SecurityGroup", StringComparison.OrdinalIgnoreCase);
        }

        private static object FindIndexed(object collection, string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("Name is required.");

            try
            {
                return collection.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, collection, new object[] { name });
            }
            catch
            {
                foreach (var item in Enumerate(collection))
                    if (string.Equals(ReadString(item, "Name"), name, StringComparison.OrdinalIgnoreCase)
                        || string.Equals(ReadString(item, "RoleName"), name, StringComparison.OrdinalIgnoreCase)
                        || string.Equals(ReadString(item, "UserName"), name, StringComparison.OrdinalIgnoreCase)
                        || string.Equals(ReadString(item, "GroupName"), name, StringComparison.OrdinalIgnoreCase))
                        return item;
            }

            throw new InvalidOperationException($"'{name}' was not found.");
        }

        private static IEnumerable<object> Enumerate(object collection)
        {
            if (collection == null) yield break;

            var count = ReadProperty(collection, "count");
            if (count != null)
            {
                for (var i = 1; i <= Convert.ToInt32(count, CultureInfo.InvariantCulture); i++)
                {
                    var item = collection.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, collection, new object[] { i });
                    if (item != null)
                        yield return item;
                }

                yield break;
            }

            if (collection is IEnumerable enumerable)
            {
                foreach (var item in enumerable)
                    if (item != null)
                        yield return item;
            }
        }

        private static IReadOnlyList<T> TryReadList<T>(Func<IReadOnlyList<T>> read, List<LlmUnavailableField> unavailable, string field)
        {
            try
            {
                return read();
            }
            catch (Exception ex)
            {
                unavailable.Add(new LlmUnavailableField { Field = field, Reason = ex.Message });
                return Array.Empty<T>();
            }
        }

        private static MxValue CreateMxValue(string value, string dataType)
        {
            var mxValue = new MxValueClass();
            switch (Normalize(dataType))
            {
                case "bool":
                case "boolean":
                case "mxboolean":
                    mxValue.PutBoolean(bool.Parse(value));
                    break;
                case "int":
                case "integer":
                case "mxinteger":
                    mxValue.PutInteger(int.Parse(value, CultureInfo.InvariantCulture));
                    break;
                case "float":
                case "mxfloat":
                    mxValue.PutFloat(float.Parse(value, CultureInfo.InvariantCulture));
                    break;
                case "double":
                case "mxdouble":
                    mxValue.PutDouble(double.Parse(value, CultureInfo.InvariantCulture));
                    break;
                case "ms":
                case "millisecond":
                case "milliseconds":
                case "elapsed-ms":
                case "mxelapsedtime-ms":
                    var milliseconds = double.Parse(value, CultureInfo.InvariantCulture);
                    var elapsedMs = Int64ToElapsedTime(Convert.ToInt64(milliseconds * 10000d));
                    mxValue.PutElapsedTime(ref elapsedMs);
                    break;
                case "elapsed":
                case "elapsedtime":
                case "elapsed-time":
                case "mxelapsedtime":
                    var elapsed = Int64ToElapsedTime(long.Parse(value, CultureInfo.InvariantCulture));
                    mxValue.PutElapsedTime(ref elapsed);
                    break;
                default:
                    mxValue.PutString(value);
                    break;
            }

            return (MxValue)mxValue;
        }

        private static MxAttributeCategory UdaCategory(string value)
        {
            var category = EnumValue<MxAttributeCategory>(value);

            // Some GRAccess builds expose non-lockable U/S/C categories in the enum
            // but reject them for AddUDA/UpdateUDA. Treat the CLI's writeable-USC
            // default as the closest accepted UDA category on those systems.
            return category == MxAttributeCategory.MxCategoryWriteable_USC
                ? MxAttributeCategory.MxCategoryWriteable_USC_Lockable
                : category;
        }

        private static string ExtensionType(string value)
        {
            if (string.Equals(value, "AnalogLimitAlarm", StringComparison.OrdinalIgnoreCase))
                return "AnalogExtension";

            return value;
        }

        private static VB_LARGE_INTEGER Int64ToElapsedTime(long value)
        {
            return new VB_LARGE_INTEGER
            {
                LowPart = unchecked((int)(value & 0xFFFFFFFF)),
                HighPart = unchecked((int)(value >> 32))
            };
        }

        private static long ElapsedTimeToInt64(VB_LARGE_INTEGER value)
        {
            return ((long)value.HighPart << 32) | (uint)value.LowPart;
        }

        private static object[] DeployArgs(IDictionary<string, object> args)
        {
            return new[]
            {
                (object)EnumValue<EActionForCurrentlyDeployedObjects>(Arg(args, "deployed-action", "deployChanges")),
                EnumValue<ESkipIfCurrentlyUndeployed>(Arg(args, "skip-undeployed", "dontSkipIfCurrentlyUndeployed")),
                EnumValue<EDeployOnScan>(Arg(args, "on-scan", "doDeployOnScan")),
                EnumValue<EForceOffScan>(Arg(args, "force-off-scan", "doForceOffScan")),
                EnumValue<ECascade>(Arg(args, "cascade", "doCascade")),
                BoolArg(args, "mark-on-mismatch")
            };
        }

        private static object[] UndeployArgs(IDictionary<string, object> args)
        {
            return new[]
            {
                (object)EnumValue<EForceOffScan>(Arg(args, "force-off-scan", "dontForceOffScan")),
                EnumValue<ECascade>(Arg(args, "cascade", "doCascade")),
                BoolArg(args, "mark-on-failure")
            };
        }

        private static object[] UploadArgs(IDictionary<string, object> args)
        {
            return new[]
            {
                (object)EnumValue<EAutomaticallyUndocheckout>(Arg(args, "auto-undo-checkout", "dontAutomaticallyUndocheckout")),
                EnumValue<ESkipOtherUsersCheckedOutObjects>(Arg(args, "skip-checked-out", "doSkipOtherUsersCheckedOutObjects")),
                EnumValue<ESkipObjectsWithPendingUpdates>(Arg(args, "skip-pending", "doSkipObjectsWithPendingUpdates"))
            };
        }

        private static object EnumValue(string typeName, string value)
        {
            var enumType = typeof(IGalaxy).Assembly.GetTypes().FirstOrDefault(t => t.Name == typeName);
            if (enumType == null)
                throw new InvalidOperationException($"Enum '{typeName}' was not found.");

            return EnumValue(enumType, value);
        }

        private static TEnum EnumValue<TEnum>(string value) where TEnum : struct
        {
            var enumType = typeof(TEnum);
            if (!enumType.IsEnum)
                throw new InvalidOperationException($"Type '{enumType.Name}' is not an enum.");

            return (TEnum)EnumValue(enumType, value);
        }

        private static object EnumValue(Type enumType, string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                return Enum.GetValues(enumType).GetValue(0);

            foreach (var name in Enum.GetNames(enumType))
            {
                if (string.Equals(name, value, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(name, "galaxy_" + value, StringComparison.OrdinalIgnoreCase)
                    || (value.StartsWith("galaxy_", StringComparison.OrdinalIgnoreCase)
                        && string.Equals(name, value.Substring("galaxy_".Length), StringComparison.OrdinalIgnoreCase)))
                    return Enum.Parse(enumType, name);
            }

            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric))
                return Enum.ToObject(enumType, numeric);

            throw new ArgumentException($"'{value}' is not a valid {enumType.Name} value.");
        }

        private static void RequireConfirm(IDictionary<string, object> args, string expectedTarget)
        {
            if (!BoolArg(args, "confirm"))
                throw new InvalidOperationException("This command requires --confirm.");

            var confirmTarget = Arg(args, "confirm-target", string.Empty);
            if (!string.IsNullOrWhiteSpace(expectedTarget)
                && !string.Equals(confirmTarget, expectedTarget, StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException($"This command requires --confirm-target {expectedTarget}.");
        }

        private static GRAccessObjectKind Kind(IDictionary<string, object> args)
        {
            return ParseKind(Arg(args, "type", "all"));
        }

        private static GRAccessObjectKind ParseKind(string value)
        {
            return GRAccessQueryCommandHandler.ParseKind(value);
        }

        private static string BulkTarget(IDictionary<string, object> args)
        {
            var names = List(args, "name").ToList();
            if (names.Any()) return string.Join(",", names);
            var patterns = List(args, "pattern").ToList();
            return string.Join(",", patterns);
        }

        private static Dictionary<string, string> ToStringArgs(IDictionary<string, object> args)
        {
            return args.ToDictionary(kvp => kvp.Key, kvp => Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) ?? string.Empty);
        }

        private static string Arg(IDictionary<string, object> args, string key, string defaultValue = null)
        {
            if (args == null || !args.TryGetValue(key, out var value) || value == null)
            {
                if (defaultValue != null) return defaultValue;
                throw new ArgumentException($"--{key} is required.");
            }

            if (value is JValue jValue)
                return Convert.ToString(jValue.Value, CultureInfo.InvariantCulture);

            return Convert.ToString(value, CultureInfo.InvariantCulture);
        }

        private static string OptionalArg(IDictionary<string, object> args, string key)
        {
            if (args == null || !args.TryGetValue(key, out var value) || value == null)
                return string.Empty;

            if (value is JValue jValue)
                value = jValue.Value;

            return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
        }

        private static bool BoolArg(IDictionary<string, object> args, string key)
        {
            if (args == null || !args.TryGetValue(key, out var value) || value == null) return false;
            if (value is JValue jValue) value = jValue.Value;
            return value is bool b ? b : bool.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), out var parsed) && parsed;
        }

        private static int IntArg(IDictionary<string, object> args, string key, int defaultValue)
        {
            if (args == null || !args.TryGetValue(key, out var value) || value == null) return defaultValue;
            if (value is JValue jValue) value = jValue.Value;
            return int.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : defaultValue;
        }

        private static IEnumerable<string> List(IDictionary<string, object> args, string key)
        {
            if (args == null || !args.TryGetValue(key, out var value) || value == null)
                yield break;

            if (value is JArray array)
            {
                foreach (var item in array)
                    yield return item.ToString();
                yield break;
            }

            if (value is IEnumerable enumerable && !(value is string))
            {
                foreach (var item in enumerable)
                    if (item != null)
                        yield return Convert.ToString(item, CultureInfo.InvariantCulture);
                yield break;
            }

            foreach (var item in Convert.ToString(value, CultureInfo.InvariantCulture).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                yield return item.Trim();
        }

        private static bool Json(IDictionary<string, object> args) => BoolArg(args, "json");

        private static bool LlmJson(IDictionary<string, object> args) => BoolArg(args, "llm-json");

        private static string Normalize(string value) => (value ?? string.Empty).Trim().ToLowerInvariant();

        private static string ToPascal(string value)
        {
            if (string.IsNullOrWhiteSpace(value)) return value;
            return string.Concat(value.Split('-').Select(part => char.ToUpperInvariant(part[0]) + part.Substring(1)));
        }

        private static string PropertyName(string value)
        {
            switch (Normalize(value))
            {
                case "tagname": return "Tagname";
                case "contained-name": return "ContainedName";
                case "area": return "Area";
                case "host": return "Host";
                case "container": return "Container";
                case "toolset": return "Toolset";
                case "security-group": return "SecurityGroup";
                default: throw new ArgumentException("Property must be one of: tagname, contained-name, area, host, container, toolset, security-group.");
            }
        }

        private static string ReadString(object target, string property)
        {
            return Convert.ToString(ReadProperty(target, property), CultureInfo.InvariantCulture) ?? string.Empty;
        }

        private static string Try(Func<string> read)
        {
            try { return read() ?? string.Empty; }
            catch { return string.Empty; }
        }

        private static bool StringEquals(string left, string right)
        {
            return string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
        }

        private static string RootMessage(Exception ex)
        {
            while (ex is TargetInvocationException && ex.InnerException != null)
                ex = ex.InnerException;
            return ex.Message;
        }

        private static T? TryNullable<T>(Func<T> read) where T : struct
        {
            try { return read(); }
            catch { return null; }
        }
    }
}
