using System.CommandLine;
using System.CommandLine.Parsing;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;

public static class TemplateCommands
{
    /// <summary>Builds the <c>template</c> command with its subcommands using the given shared CLI options.</summary>
    /// <param name="urlOption">Shared management URL option.</param>
    /// <param name="formatOption">Shared output format option.</param>
    /// <param name="usernameOption">Shared username option for authentication.</param>
    /// <param name="passwordOption">Shared password option for authentication.</param>
    /// <returns>The fully configured <c>template</c> command with all its subcommands.</returns>
    public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var command = new Command("template") { Description = "Manage templates" };

        command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildValidate(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildAttribute(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildAlarm(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildNativeAlarmSource(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildScript(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildComposition(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildFolder(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildResyncMembers(urlOption, formatOption, usernameOption, passwordOption));

        return command;
    }

    /// <summary>
    /// Builds the <c>template folder</c> subgroup — the CLI surface for the template
    /// folder hierarchy management commands (folders are a Central UI organizational
    /// device only; they have no effect on template resolution or flattening).
    /// Omitting <c>--parent-id</c> on create/move places the folder at the tree root,
    /// matching the nullable <c>ParentFolderId</c> on the underlying commands.
    /// </summary>
    private static Command BuildFolder(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("folder") { Description = "Manage template folders" };

        var listCmd = new Command("list") { Description = "List all template folders" };
        listCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new ListTemplateFoldersCommand());
        });
        group.Add(listCmd);

        var createNameOption = new Option<string>("--name") { Description = "Folder name", Required = true };
        var createParentOption = new Option<int?>("--parent-id") { Description = "Parent folder ID; omit to create at the tree root" };
        var createCmd = new Command("create") { Description = "Create a template folder" };
        createCmd.Add(createNameOption);
        createCmd.Add(createParentOption);
        createCmd.SetAction(async (ParseResult result) =>
        {
            var name = result.GetValue(createNameOption)!;
            var parentId = result.GetValue(createParentOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new CreateTemplateFolderCommand(name, parentId));
        });
        group.Add(createCmd);

        var renameIdOption = new Option<int>("--id") { Description = "Folder ID", Required = true };
        var renameNameOption = new Option<string>("--name") { Description = "New folder name", Required = true };
        var renameCmd = new Command("rename") { Description = "Rename a template folder" };
        renameCmd.Add(renameIdOption);
        renameCmd.Add(renameNameOption);
        renameCmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(renameIdOption);
            var name = result.GetValue(renameNameOption)!;
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new RenameTemplateFolderCommand(id, name));
        });
        group.Add(renameCmd);

        var moveIdOption = new Option<int>("--id") { Description = "Folder ID", Required = true };
        var moveParentOption = new Option<int?>("--parent-id") { Description = "New parent folder ID; omit to move to the tree root" };
        var moveCmd = new Command("move") { Description = "Move a template folder under a new parent folder (or to the tree root)" };
        moveCmd.Add(moveIdOption);
        moveCmd.Add(moveParentOption);
        moveCmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(moveIdOption);
            var parentId = result.GetValue(moveParentOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new MoveTemplateFolderCommand(id, parentId));
        });
        group.Add(moveCmd);

        var reorderIdOption = new Option<int>("--id") { Description = "Folder ID", Required = true };
        var reorderDirectionOption = new Option<string>("--direction")
        {
            Description = "Reorder direction: 'up' swaps with the previous sibling, 'down' with the next",
            Required = true
        };
        reorderDirectionOption.AcceptOnlyFromAmong("up", "down");
        var reorderCmd = new Command("reorder") { Description = "Reorder a template folder among its siblings" };
        reorderCmd.Add(reorderIdOption);
        reorderCmd.Add(reorderDirectionOption);
        reorderCmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(reorderIdOption);
            // AcceptOnlyFromAmong has already rejected anything but the two literals,
            // so this maps a validated value rather than parsing untrusted input.
            var direction = result.GetValue(reorderDirectionOption) == "up"
                ? ReorderDirection.Up
                : ReorderDirection.Down;

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new ReorderTemplateFolderCommand(id, direction));
        });
        group.Add(reorderCmd);

        var deleteIdOption = new Option<int>("--id") { Description = "Folder ID", Required = true };
        var deleteCmd = new Command("delete") { Description = "Delete a template folder (fails if it contains subfolders or templates)" };
        deleteCmd.Add(deleteIdOption);
        deleteCmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(deleteIdOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateFolderCommand(id));
        });
        group.Add(deleteCmd);

        return group;
    }

    private static Command BuildResyncMembers(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var idOption = new Option<int>("--id") { Description = "Template ID (its derived subtree is included)", Required = true };
        var cmd = new Command("resync-members")
        {
            Description = "Resync inherited members onto a template and its derived subtree " +
                          "(materialize missing, re-sync drifted, remove orphaned)"
        };
        cmd.Add(idOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(idOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new ResyncInheritedMembersCommand(id));
        });
        return cmd;
    }

    private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        // The server projects this listing in the database and returns
        // TemplateSummary rows (counts, no child collections), so --detail cannot
        // produce full definitions here — it only skips the CLI-side column
        // projection and renders the summary payload verbatim. Full definitions
        // come from `template get --id`.
        var detailOption = new Option<bool>("--detail")
        {
            Description = "Render the raw list payload in table output instead of the compact column projection. "
                + "The server returns summary rows (member counts only) for a listing, so this does NOT include "
                + "attribute/alarm/script definitions — use 'template get --id <int>' for those. No effect on JSON output."
        };
        var skipOption = new Option<int>("--skip") { Description = "Offset paging: number of items to skip (default 0)" };
        var takeOption = new Option<int?>("--take") { Description = "Offset paging: max items to return (1..1000; omit for unlimited)" };
        var cmd = new Command("list") { Description = "List all templates (compact table summary with member counts; definitions via 'template get')" };
        cmd.Add(detailOption);
        cmd.Add(skipOption);
        cmd.Add(takeOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var detail = result.GetValue(detailOption);
            var skip = result.GetValue(skipOption);
            var take = result.GetValue(takeOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new ListTemplatesCommand(skip, take),
                tableProjector: detail ? null : TemplateTableProjection.ProjectSummary);
        });
        return cmd;
    }

    private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
        var detailOption = new Option<bool>("--detail")
        {
            Description = "Include full template definitions (all attributes/alarms/scripts) in table output. "
                + "Without it, table output is a compact summary (counts only). JSON output is always full."
        };
        var cmd = new Command("get") { Description = "Get a template by ID (compact table summary; use --detail for the full dump)" };
        cmd.Add(idOption);
        cmd.Add(detailOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(idOption);
            var detail = result.GetValue(detailOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new GetTemplateCommand(id),
                tableProjector: detail ? null : TemplateTableProjection.ProjectSummary);
        });
        return cmd;
    }

    private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var nameOption = new Option<string>("--name") { Description = "Template name", Required = true };
        var descOption = new Option<string?>("--description") { Description = "Template description" };
        var parentOption = new Option<int?>("--parent-id") { Description = "Parent template ID" };

        var cmd = new Command("create") { Description = "Create a new template" };
        cmd.Add(nameOption);
        cmd.Add(descOption);
        cmd.Add(parentOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var name = result.GetValue(nameOption)!;
            var desc = result.GetValue(descOption);
            var parentId = result.GetValue(parentOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new CreateTemplateCommand(name, desc, parentId));
        });
        return cmd;
    }

    private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
        var nameOption = new Option<string>("--name") { Description = "Template name", Required = true };
        var descOption = new Option<string?>("--description") { Description = "Template description. Omit to leave unchanged; pass an empty string (\"\") to clear it." };
        var parentOption = new Option<int?>("--parent-id") { Description = "Parent template ID. Immutable after creation; omit to leave unchanged." };

        var cmd = new Command("update") { Description = "Update a template (omitted optional fields are left unchanged)" };
        cmd.Add(idOption);
        cmd.Add(nameOption);
        cmd.Add(descOption);
        cmd.Add(parentOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(idOption);
            var name = result.GetValue(nameOption)!;
            var desc = result.GetValue(descOption);
            var parentId = result.GetValue(parentOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new UpdateTemplateCommand(id, name, desc, parentId));
        });
        return cmd;
    }

    private static Command BuildValidate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
        var cmd = new Command("validate") { Description = "Validate a template" };
        cmd.Add(idOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(idOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new ValidateTemplateCommand(id));
        });
        return cmd;
    }

    private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
        var cmd = new Command("delete") { Description = "Delete a template" };
        cmd.Add(idOption);
        cmd.SetAction(async (ParseResult result) =>
        {
            var id = result.GetValue(idOption);
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption, new DeleteTemplateCommand(id));
        });
        return cmd;
    }

    private static Command BuildAttribute(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("attribute") { Description = "Manage template attributes" };

        var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var nameOption = new Option<string>("--name") { Description = "Attribute name", Required = true };
        var dataTypeOption = new Option<string>("--data-type") { Description = "Data type", Required = true };
        var valueOption = new Option<string?>("--value") { Description = "Default value. For a List attribute, supply a JSON array in native form: numeric/boolean elements unquoted (e.g. an Int32 list '[10,20,30]'), string elements quoted (e.g. '[\"WO-1\",\"WO-2\"]')." };
        var descOption = new Option<string?>("--description") { Description = "Description" };
        var sourceOption = new Option<string?>("--data-source") { Description = "Data source reference" };
        var elementTypeOption = new Option<string?>("--element-type") { Description = ElementTypeOptionDescription };
        var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        lockedOption.DefaultValueFactory = _ => false;

        var addCmd = new Command("add") { Description = "Add an attribute to a template" };
        addCmd.Add(templateIdOption);
        addCmd.Add(nameOption);
        addCmd.Add(dataTypeOption);
        addCmd.Add(valueOption);
        addCmd.Add(descOption);
        addCmd.Add(sourceOption);
        addCmd.Add(elementTypeOption);
        addCmd.Add(lockedOption);
        addCmd.SetAction(async (ParseResult result) =>
        {
            var dataType = result.GetValue(dataTypeOption)!;
            var elementType = result.GetValue(elementTypeOption);
            if (!TryValidateElementType(dataType, elementType, out var error))
            {
                OutputFormatter.WriteError(error!, "INVALID_ARGUMENT");
                return 1;
            }

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                BuildAddAttributeCommand(
                    result.GetValue(templateIdOption),
                    result.GetValue(nameOption)!,
                    dataType,
                    result.GetValue(valueOption),
                    result.GetValue(descOption),
                    result.GetValue(sourceOption),
                    result.GetValue(lockedOption),
                    elementType));
        });
        group.Add(addCmd);

        var updateIdOption = new Option<int>("--id") { Description = "Attribute ID", Required = true };
        var updateNameOption = new Option<string>("--name") { Description = "Attribute name", Required = true };
        var updateDataTypeOption = new Option<string>("--data-type") { Description = "Data type", Required = true };
        var updateValueOption = new Option<string?>("--value") { Description = "Default value. For a List attribute, supply a JSON array in native form: numeric/boolean elements unquoted (e.g. an Int32 list '[10,20,30]'), string elements quoted (e.g. '[\"WO-1\",\"WO-2\"]')." };
        var updateDescOption = new Option<string?>("--description") { Description = "Description" };
        var updateSourceOption = new Option<string?>("--data-source") { Description = "Data source reference" };
        var updateElementTypeOption = new Option<string?>("--element-type") { Description = ElementTypeOptionDescription };
        var updateLockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        updateLockedOption.DefaultValueFactory = _ => false;

        var updateCmd = new Command("update") { Description = "Update a template attribute" };
        updateCmd.Add(updateIdOption);
        updateCmd.Add(updateNameOption);
        updateCmd.Add(updateDataTypeOption);
        updateCmd.Add(updateValueOption);
        updateCmd.Add(updateDescOption);
        updateCmd.Add(updateSourceOption);
        updateCmd.Add(updateElementTypeOption);
        updateCmd.Add(updateLockedOption);
        updateCmd.SetAction(async (ParseResult result) =>
        {
            var dataType = result.GetValue(updateDataTypeOption)!;
            var elementType = result.GetValue(updateElementTypeOption);
            if (!TryValidateElementType(dataType, elementType, out var error))
            {
                OutputFormatter.WriteError(error!, "INVALID_ARGUMENT");
                return 1;
            }

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                BuildUpdateAttributeCommand(
                    result.GetValue(updateIdOption),
                    result.GetValue(updateNameOption)!,
                    dataType,
                    result.GetValue(updateValueOption),
                    result.GetValue(updateDescOption),
                    result.GetValue(updateSourceOption),
                    result.GetValue(updateLockedOption),
                    elementType));
        });
        group.Add(updateCmd);

        var deleteIdOption = new Option<int>("--id") { Description = "Attribute ID", Required = true };
        var deleteCmd = new Command("delete") { Description = "Delete a template attribute" };
        deleteCmd.Add(deleteIdOption);
        deleteCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateAttributeCommand(result.GetValue(deleteIdOption)));
        });
        group.Add(deleteCmd);

        return group;
    }

    /// <summary>Shared description for the <c>--element-type</c> option on attribute add/update.</summary>
    internal const string ElementTypeOptionDescription =
        "Element scalar type for a List attribute (String, Int32, Float, Double, Boolean, DateTime). Required when --data-type is List.";

    /// <summary>Shared description for the <c>--min-time-between-runs</c> option on script add/update.</summary>
    internal const string MinTimeBetweenRunsOptionDescription =
        "Minimum time between script runs (a throttle for triggered scripts; the re-fire interval for a WhileTrue trigger). "
        + "Accepts a value with a unit suffix: ms, s/sec, or m/min (e.g. 500ms, 5s, 2min); a bare number is seconds. "
        + "Omit, or pass 0 / a blank value, to leave it unset (no throttle).";

    /// <summary>Shared description for the <c>--execution-timeout-seconds</c> option on script add/update.</summary>
    internal const string ExecutionTimeoutOptionDescription =
        "Per-script execution timeout in seconds. Omit (or pass a non-positive value) to use the site's global "
        + "default (SiteRuntimeOptions.ScriptExecutionTimeoutSeconds).";

    /// <summary>Shared description for the script <c>--trigger-kind</c> option on script add/update.</summary>
    internal const string ScriptTriggerKindOptionDescription =
        "Expression trigger analysis kind: advisory (default) or strict. "
        + "Strict escalates a blank expression from a warning to a deploy-blocking error. "
        + "Requires --trigger-config: with no config the kind is ignored (a warning is printed to stderr).";

    /// <summary>Shared description for the alarm <c>--trigger-kind</c> option on alarm update.</summary>
    internal const string AlarmTriggerKindOptionDescription =
        "Expression trigger analysis kind: advisory (default) or strict. "
        + "Requires --trigger-config: with no config the kind is ignored (a warning is printed to stderr).";

    /// <summary>
    /// The element scalar types permitted for a List attribute — derived from the
    /// single source of truth, <see cref="AttributeValueCodec.IsValidElementType"/>,
    /// so the CLI never drifts from the codec/Management API.
    /// </summary>
    private static readonly string[] ValidElementScalars =
        Enum.GetValues<DataType>()
            .Where(AttributeValueCodec.IsValidElementType)
            .Select(t => t.ToString())
            .ToArray();

    /// <summary>
    /// Validates the <c>--data-type</c> / <c>--element-type</c> combination client-side so
    /// the CLI fails fast with a clear message before contacting the Management API (the
    /// server validates independently). A List attribute requires a valid element scalar;
    /// a non-List attribute must not carry an element type. Comparison is case-insensitive.
    /// </summary>
    /// <param name="dataType">The raw <c>--data-type</c> value.</param>
    /// <param name="elementType">The raw <c>--element-type</c> value, or null if absent.</param>
    /// <param name="error">A descriptive error message when validation fails; otherwise null.</param>
    /// <returns><c>true</c> when the combination is valid; otherwise <c>false</c>.</returns>
    internal static bool TryValidateElementType(string dataType, string? elementType, out string? error)
    {
        error = null;
        var isList = string.Equals(dataType, "List", StringComparison.OrdinalIgnoreCase);
        var hasElementType = !string.IsNullOrWhiteSpace(elementType);

        if (isList)
        {
            if (!hasElementType)
            {
                error = "--element-type is required when --data-type is List "
                    + "(one of: String, Int32, Float, Double, Boolean, DateTime).";
                return false;
            }

            if (!Enum.TryParse<DataType>(elementType!.Trim(), ignoreCase: true, out var parsed)
                || !AttributeValueCodec.IsValidElementType(parsed))
            {
                error = $"Invalid --element-type '{elementType}'. Valid List element scalars are: "
                    + string.Join(", ", ValidElementScalars) + ".";
                return false;
            }

            return true;
        }

        if (hasElementType)
        {
            error = "--element-type is only valid when --data-type is List.";
            return false;
        }

        return true;
    }

    /// <summary>
    /// Builds the <see cref="AddTemplateAttributeCommand"/> payload sent to the Management API.
    /// The raw <paramref name="value"/> string is forwarded unchanged — for a List attribute it
    /// is a JSON array, which the API/codec parses; the CLI does not reshape it.
    /// </summary>
    /// <param name="templateId">The template to add the attribute to.</param>
    /// <param name="name">The attribute name.</param>
    /// <param name="dataType">The attribute's data type.</param>
    /// <param name="value">The raw attribute value, forwarded unchanged.</param>
    /// <param name="description">An optional human-readable description.</param>
    /// <param name="dataSource">An optional data source binding for the attribute.</param>
    /// <param name="isLocked">Whether the attribute is locked from override in derived templates.</param>
    /// <param name="elementType">The element type for a List-typed attribute; otherwise null.</param>
    /// <returns>The command payload to send to the Management API.</returns>
    internal static AddTemplateAttributeCommand BuildAddAttributeCommand(
        int templateId, string name, string dataType, string? value,
        string? description, string? dataSource, bool isLocked, string? elementType)
        => new(templateId, name, dataType, value, description, dataSource, isLocked, NormalizeElementType(elementType));

    /// <summary>
    /// Builds the <see cref="UpdateTemplateAttributeCommand"/> payload sent to the Management API.
    /// The raw <paramref name="value"/> string is forwarded unchanged — for a List attribute it
    /// is a JSON array, which the API/codec parses; the CLI does not reshape it.
    /// </summary>
    /// <param name="attributeId">The attribute to update.</param>
    /// <param name="name">The attribute name.</param>
    /// <param name="dataType">The attribute's data type.</param>
    /// <param name="value">The raw attribute value, forwarded unchanged.</param>
    /// <param name="description">An optional human-readable description.</param>
    /// <param name="dataSource">An optional data source binding for the attribute.</param>
    /// <param name="isLocked">Whether the attribute is locked from override in derived templates.</param>
    /// <param name="elementType">The element type for a List-typed attribute; otherwise null.</param>
    /// <returns>The command payload to send to the Management API.</returns>
    internal static UpdateTemplateAttributeCommand BuildUpdateAttributeCommand(
        int attributeId, string name, string dataType, string? value,
        string? description, string? dataSource, bool isLocked, string? elementType)
        => new(attributeId, name, dataType, value, description, dataSource, isLocked, NormalizeElementType(elementType));

    /// <summary>Trims a non-empty element type; an empty/whitespace value becomes null (no element type).</summary>
    private static string? NormalizeElementType(string? elementType)
        => string.IsNullOrWhiteSpace(elementType) ? null : elementType.Trim();

    /// <summary>
    /// Parses the raw <c>--min-time-between-runs</c> value into a <see cref="TimeSpan"/>.
    /// A blank/absent value means "unset" (<paramref name="value"/> null → <c>null</c>, no error).
    /// Otherwise the value must be a positive integer with an optional unit suffix:
    /// <c>ms</c>, <c>s</c>/<c>sec</c>, or <c>m</c>/<c>min</c>; a bare number is interpreted as
    /// seconds (matching the UI duration input's default unit). A value of <c>0</c> resolves to
    /// <c>null</c> (unset), mirroring <c>DurationInput.Compose</c>. The unit set and "bare = seconds"
    /// default deliberately mirror the Central UI duration input so the two authoring surfaces agree.
    /// </summary>
    /// <param name="value">The raw flag value, or null when the flag was omitted.</param>
    /// <param name="duration">The parsed duration, or <c>null</c> for an absent/unset value.</param>
    /// <param name="error">A descriptive error message when parsing fails; otherwise null.</param>
    /// <returns><c>true</c> when the value is absent or parses cleanly; <c>false</c> on a malformed value.</returns>
    internal static bool TryParseMinTimeBetweenRuns(string? value, out TimeSpan? duration, out string? error)
    {
        duration = null;
        error = null;

        var trimmed = value?.Trim();
        if (string.IsNullOrEmpty(trimmed))
            return true; // omitted → unset

        // Split a trailing alphabetic unit suffix from the leading numeric part.
        var splitIndex = trimmed.Length;
        while (splitIndex > 0 && char.IsLetter(trimmed[splitIndex - 1]))
            splitIndex--;

        var numberPart = trimmed[..splitIndex];
        var unitPart = trimmed[splitIndex..].ToLowerInvariant();

        if (!long.TryParse(numberPart, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var amount)
            || amount < 0)
        {
            error = $"Invalid --min-time-between-runs '{value}'. Expected a non-negative number with an optional "
                + "unit suffix (ms, s/sec, m/min); a bare number is seconds (e.g. 500ms, 5s, 2min).";
            return false;
        }

        var factorMs = unitPart switch
        {
            "ms" => 1L,
            "" or "s" or "sec" => 1000L,
            "m" or "min" => 60000L,
            _ => -1L,
        };

        if (factorMs < 0)
        {
            error = $"Invalid --min-time-between-runs unit in '{value}'. Valid units are: ms, s/sec, m/min.";
            return false;
        }

        // Guard against overflow: reject if amount * factorMs would exceed TimeSpan.MaxValue (in ms).
        // The divide is safe because factorMs is always >= 1.
        if (amount > TimeSpan.MaxValue.TotalMilliseconds / factorMs)
        {
            error = $"Invalid --min-time-between-runs '{value}': value is too large.";
            return false;
        }

        // 0 (with or without a unit) → unset, mirroring DurationInput.Compose's non-positive handling.
        duration = amount == 0 ? null : TimeSpan.FromMilliseconds(amount * factorMs);
        return true;
    }

    /// <summary>
    /// Determines whether <c>--trigger-kind</c> was supplied without a <c>--trigger-config</c>.
    /// In that case <see cref="TriggerConfigJson.InjectAnalysisKind"/> has no JSON object to inject
    /// into and silently drops the kind — the caller should warn the user.
    /// </summary>
    /// <param name="triggerConfig">The raw <c>--trigger-config</c> JSON value, or null when absent.</param>
    /// <param name="triggerKind">The raw <c>--trigger-kind</c> value, or null when absent.</param>
    /// <returns><c>true</c> when a kind was given but no config — i.e. the kind will be ignored.</returns>
    internal static bool TriggerKindWillBeIgnored(string? triggerConfig, string? triggerKind)
        => !string.IsNullOrWhiteSpace(triggerKind) && string.IsNullOrWhiteSpace(triggerConfig);

    /// <summary>
    /// Warns (to stderr) when <c>--trigger-kind</c> is supplied without a <c>--trigger-config</c>
    /// (see <see cref="TriggerKindWillBeIgnored"/>). The command still proceeds — the kind is
    /// advisory metadata, not a required field — but the user is told it had no effect so they are
    /// not surprised that a <c>strict</c> request did not take. Warn-and-continue (rather than a hard
    /// error) keeps the flag combination non-fatal: a script/alarm is still created, just without the
    /// requested analysis kind.
    /// </summary>
    /// <param name="triggerConfig">The raw <c>--trigger-config</c> JSON value, or null when absent.</param>
    /// <param name="triggerKind">The raw <c>--trigger-kind</c> value, or null when absent.</param>
    private static void WarnIfTriggerKindIgnored(string? triggerConfig, string? triggerKind)
    {
        if (TriggerKindWillBeIgnored(triggerConfig, triggerKind))
        {
            Console.Error.WriteLine(
                $"warning: --trigger-kind '{triggerKind!.Trim()}' was ignored because no --trigger-config was supplied. "
                + "The analysis kind is written into the trigger-config JSON, so it has no effect without one.");
        }
    }

    private static Command BuildAlarm(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("alarm") { Description = "Manage template alarms" };

        var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var nameOption = new Option<string>("--name") { Description = "Alarm name", Required = true };
        var triggerTypeOption = new Option<string>("--trigger-type") { Description = "Trigger type", Required = true };
        var priorityOption = new Option<int>("--priority") { Description = "Alarm priority", Required = true };
        var descOption = new Option<string?>("--description") { Description = "Description" };
        var triggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
        var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        lockedOption.DefaultValueFactory = _ => false;

        // Typed setpoint flags (alternative to raw --trigger-config; raw wins when both supplied).
        var attributeOption = new Option<string?>("--attribute") { Description = "Attribute name the trigger watches (all trigger types except Expression)" };
        var matchValueOption = new Option<string?>("--match-value") { Description = "ValueMatch: value to compare against" };
        var notEqualsOption = new Option<bool>("--not-equals") { Description = "ValueMatch: match when the value is NOT equal (emits !=)" };
        notEqualsOption.DefaultValueFactory = _ => false;
        var minOption = new Option<double?>("--min") { Description = "RangeViolation: minimum allowed value" };
        var maxOption = new Option<double?>("--max") { Description = "RangeViolation: maximum allowed value" };
        var thresholdOption = new Option<double?>("--threshold-per-second") { Description = "RateOfChange: rate threshold per second" };
        var windowOption = new Option<double?>("--window-seconds") { Description = "RateOfChange: sliding window in seconds" };
        var directionOption = new Option<string?>("--direction") { Description = "RateOfChange: direction (rising|falling|either)" };
        var loLoOption = new Option<double?>("--lolo") { Description = "HiLo: low-low setpoint" };
        var loOption = new Option<double?>("--lo") { Description = "HiLo: low setpoint" };
        var hiOption = new Option<double?>("--hi") { Description = "HiLo: high setpoint" };
        var hiHiOption = new Option<double?>("--hihi") { Description = "HiLo: high-high setpoint" };
        var expressionOption = new Option<string?>("--expression") { Description = "Expression: boolean trigger expression" };
        // Analysis kind for Expression triggers (advisory|strict; default advisory).
        // Writes "analysisKind":"Strict" into the trigger config when set to strict.
        var triggerKindOption = new Option<string?>("--trigger-kind")
        {
            Description = "Expression trigger analysis kind: advisory (default) or strict. " +
                          "Strict escalates a blank expression from a warning to a deploy-blocking error."
        };

        var addCmd = new Command("add") { Description = "Add an alarm to a template" };
        addCmd.Add(templateIdOption);
        addCmd.Add(nameOption);
        addCmd.Add(triggerTypeOption);
        addCmd.Add(priorityOption);
        addCmd.Add(descOption);
        addCmd.Add(triggerConfigOption);
        addCmd.Add(lockedOption);
        addCmd.Add(attributeOption);
        addCmd.Add(matchValueOption);
        addCmd.Add(notEqualsOption);
        addCmd.Add(minOption);
        addCmd.Add(maxOption);
        addCmd.Add(thresholdOption);
        addCmd.Add(windowOption);
        addCmd.Add(directionOption);
        addCmd.Add(loLoOption);
        addCmd.Add(loOption);
        addCmd.Add(hiOption);
        addCmd.Add(hiHiOption);
        addCmd.Add(expressionOption);
        addCmd.Add(triggerKindOption);
        addCmd.SetAction(async (ParseResult result) =>
        {
            var triggerType = result.GetValue(triggerTypeOption)!;
            var rawConfig = result.GetValue(triggerConfigOption);
            var triggerConfig = rawConfig ?? AlarmTriggerConfigJson.Build(
                triggerType,
                result.GetValue(attributeOption),
                result.GetValue(matchValueOption), result.GetValue(notEqualsOption),
                result.GetValue(minOption), result.GetValue(maxOption),
                result.GetValue(thresholdOption), result.GetValue(windowOption), result.GetValue(directionOption),
                result.GetValue(loLoOption), result.GetValue(loOption), result.GetValue(hiOption), result.GetValue(hiHiOption),
                result.GetValue(expressionOption),
                result.GetValue(triggerKindOption));

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new AddTemplateAlarmCommand(
                    result.GetValue(templateIdOption),
                    result.GetValue(nameOption)!,
                    triggerType,
                    result.GetValue(priorityOption)!,
                    result.GetValue(descOption),
                    triggerConfig,
                    result.GetValue(lockedOption)));
        });
        group.Add(addCmd);

        var updateIdOption = new Option<int>("--id") { Description = "Alarm ID", Required = true };
        var updateNameOption = new Option<string>("--name") { Description = "Alarm name", Required = true };
        var updateTriggerTypeOption = new Option<string>("--trigger-type") { Description = "Trigger type", Required = true };
        var updatePriorityOption = new Option<int>("--priority") { Description = "Alarm priority", Required = true };
        var updateDescOption = new Option<string?>("--description") { Description = "Description" };
        var updateTriggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
        var updateLockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        updateLockedOption.DefaultValueFactory = _ => false;
        // --trigger-kind for update (same semantics as add)
        var updateTriggerKindOption = new Option<string?>("--trigger-kind")
        {
            Description = AlarmTriggerKindOptionDescription
        };

        var updateCmd = new Command("update") { Description = "Update a template alarm" };
        updateCmd.Add(updateIdOption);
        updateCmd.Add(updateNameOption);
        updateCmd.Add(updateTriggerTypeOption);
        updateCmd.Add(updatePriorityOption);
        updateCmd.Add(updateDescOption);
        updateCmd.Add(updateTriggerConfigOption);
        updateCmd.Add(updateLockedOption);
        updateCmd.Add(updateTriggerKindOption);
        updateCmd.SetAction(async (ParseResult result) =>
        {
            var rawConfig = result.GetValue(updateTriggerConfigOption);
            WarnIfTriggerKindIgnored(rawConfig, result.GetValue(updateTriggerKindOption));
            var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
                rawConfig,
                result.GetValue(updateTriggerKindOption));
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new UpdateTemplateAlarmCommand(
                    result.GetValue(updateIdOption),
                    result.GetValue(updateNameOption)!,
                    result.GetValue(updateTriggerTypeOption)!,
                    result.GetValue(updatePriorityOption)!,
                    result.GetValue(updateDescOption),
                    triggerConfig,
                    result.GetValue(updateLockedOption)));
        });
        group.Add(updateCmd);

        var deleteIdOption = new Option<int>("--id") { Description = "Alarm ID", Required = true };
        var deleteCmd = new Command("delete") { Description = "Delete a template alarm" };
        deleteCmd.Add(deleteIdOption);
        deleteCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateAlarmCommand(result.GetValue(deleteIdOption)));
        });
        group.Add(deleteCmd);

        return group;
    }

    private static Command BuildNativeAlarmSource(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("native-alarm-source")
        {
            Description = "Manage template native alarm source bindings (read-only mirror of OPC UA A&C / MxGateway alarms)"
        };

        // add
        var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var nameOption = new Option<string>("--name") { Description = "Source binding name", Required = true };
        var connectionOption = new Option<string>("--connection") { Description = "Alarm-capable data connection name", Required = true };
        var sourceRefOption = new Option<string>("--source-ref") { Description = "Source reference (OPC UA SourceNode nodeId, or MxAccess object/area)", Required = true };
        var filterOption = new Option<string?>("--filter") { Description = "Optional condition filter (null = mirror all conditions under the source)" };
        var descOption = new Option<string?>("--description") { Description = "Description" };
        var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        lockedOption.DefaultValueFactory = _ => false;

        var addCmd = new Command("add") { Description = "Add a native alarm source binding to a template" };
        addCmd.Add(templateIdOption);
        addCmd.Add(nameOption);
        addCmd.Add(connectionOption);
        addCmd.Add(sourceRefOption);
        addCmd.Add(filterOption);
        addCmd.Add(descOption);
        addCmd.Add(lockedOption);
        addCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new AddTemplateNativeAlarmSourceCommand(
                    result.GetValue(templateIdOption),
                    result.GetValue(nameOption)!,
                    result.GetValue(connectionOption)!,
                    result.GetValue(sourceRefOption)!,
                    result.GetValue(filterOption),
                    result.GetValue(descOption),
                    result.GetValue(lockedOption)));
        });
        group.Add(addCmd);

        // list
        var listIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var listCmd = new Command("list") { Description = "List native alarm source bindings on a template" };
        listCmd.Add(listIdOption);
        listCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new ListTemplateNativeAlarmSourcesCommand(result.GetValue(listIdOption)));
        });
        group.Add(listCmd);

        // remove
        var removeIdOption = new Option<int>("--id") { Description = "Native alarm source ID", Required = true };
        var removeCmd = new Command("remove") { Description = "Remove a native alarm source binding from a template" };
        removeCmd.Add(removeIdOption);
        removeCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateNativeAlarmSourceCommand(result.GetValue(removeIdOption)));
        });
        group.Add(removeCmd);

        return group;
    }

    private static Command BuildScript(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("script") { Description = "Manage template scripts" };

        var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var nameOption = new Option<string>("--name") { Description = "Script name", Required = true };
        var codeOption = new Option<string>("--code") { Description = "Script code", Required = true };
        var triggerTypeOption = new Option<string>("--trigger-type") { Description = "Trigger type", Required = true };
        var triggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
        var lockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        lockedOption.DefaultValueFactory = _ => false;

        var paramsOption = new Option<string?>("--parameters") { Description = "Parameter definitions JSON" };
        var returnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
        var minTimeOption = new Option<string?>("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
        var execTimeoutOption = new Option<int?>("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
        // Analysis kind for Expression triggers (advisory|strict; default advisory).
        var scriptTriggerKindOption = new Option<string?>("--trigger-kind")
        {
            Description = ScriptTriggerKindOptionDescription
        };

        var addCmd = new Command("add") { Description = "Add a script to a template" };
        addCmd.Add(templateIdOption);
        addCmd.Add(nameOption);
        addCmd.Add(codeOption);
        addCmd.Add(triggerTypeOption);
        addCmd.Add(triggerConfigOption);
        addCmd.Add(lockedOption);
        addCmd.Add(paramsOption);
        addCmd.Add(returnOption);
        addCmd.Add(minTimeOption);
        addCmd.Add(execTimeoutOption);
        addCmd.Add(scriptTriggerKindOption);
        addCmd.SetAction(async (ParseResult result) =>
        {
            if (!TryParseMinTimeBetweenRuns(result.GetValue(minTimeOption), out var minTime, out var minTimeError))
            {
                OutputFormatter.WriteError(minTimeError!, "INVALID_ARGUMENT");
                return 1;
            }

            var rawConfig = result.GetValue(triggerConfigOption);
            WarnIfTriggerKindIgnored(rawConfig, result.GetValue(scriptTriggerKindOption));
            var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
                rawConfig,
                result.GetValue(scriptTriggerKindOption));
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new AddTemplateScriptCommand(
                    result.GetValue(templateIdOption),
                    result.GetValue(nameOption)!,
                    result.GetValue(codeOption)!,
                    result.GetValue(triggerTypeOption)!,
                    triggerConfig,
                    result.GetValue(lockedOption),
                    result.GetValue(paramsOption),
                    result.GetValue(returnOption),
                    minTime,
                    result.GetValue(execTimeoutOption)));
        });
        group.Add(addCmd);

        var updateIdOption = new Option<int>("--id") { Description = "Script ID", Required = true };
        var updateNameOption = new Option<string>("--name") { Description = "Script name", Required = true };
        var updateCodeOption = new Option<string>("--code") { Description = "Script code", Required = true };
        var updateTriggerTypeOption = new Option<string>("--trigger-type") { Description = "Trigger type", Required = true };
        var updateTriggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
        var updateLockedOption = new Option<bool>("--locked") { Description = "Lock status" };
        updateLockedOption.DefaultValueFactory = _ => false;

        var updateParamsOption = new Option<string?>("--parameters") { Description = "Parameter definitions JSON" };
        var updateReturnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
        var updateMinTimeOption = new Option<string?>("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
        var updateExecTimeoutOption = new Option<int?>("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
        // --trigger-kind for update (same semantics as add)
        var updateScriptTriggerKindOption = new Option<string?>("--trigger-kind")
        {
            Description = ScriptTriggerKindOptionDescription
        };

        var updateCmd = new Command("update") { Description = "Update a template script" };
        updateCmd.Add(updateIdOption);
        updateCmd.Add(updateNameOption);
        updateCmd.Add(updateCodeOption);
        updateCmd.Add(updateTriggerTypeOption);
        updateCmd.Add(updateTriggerConfigOption);
        updateCmd.Add(updateLockedOption);
        updateCmd.Add(updateParamsOption);
        updateCmd.Add(updateReturnOption);
        updateCmd.Add(updateMinTimeOption);
        updateCmd.Add(updateExecTimeoutOption);
        updateCmd.Add(updateScriptTriggerKindOption);
        updateCmd.SetAction(async (ParseResult result) =>
        {
            if (!TryParseMinTimeBetweenRuns(result.GetValue(updateMinTimeOption), out var minTime, out var minTimeError))
            {
                OutputFormatter.WriteError(minTimeError!, "INVALID_ARGUMENT");
                return 1;
            }

            var rawConfig = result.GetValue(updateTriggerConfigOption);
            WarnIfTriggerKindIgnored(rawConfig, result.GetValue(updateScriptTriggerKindOption));
            var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
                rawConfig,
                result.GetValue(updateScriptTriggerKindOption));
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new UpdateTemplateScriptCommand(
                    result.GetValue(updateIdOption),
                    result.GetValue(updateNameOption)!,
                    result.GetValue(updateCodeOption)!,
                    result.GetValue(updateTriggerTypeOption)!,
                    triggerConfig,
                    result.GetValue(updateLockedOption),
                    result.GetValue(updateParamsOption),
                    result.GetValue(updateReturnOption),
                    minTime,
                    result.GetValue(updateExecTimeoutOption)));
        });
        group.Add(updateCmd);

        var deleteIdOption = new Option<int>("--id") { Description = "Script ID", Required = true };
        var deleteCmd = new Command("delete") { Description = "Delete a template script" };
        deleteCmd.Add(deleteIdOption);
        deleteCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateScriptCommand(result.GetValue(deleteIdOption)));
        });
        group.Add(deleteCmd);

        return group;
    }

    private static Command BuildComposition(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
    {
        var group = new Command("composition") { Description = "Manage template compositions" };

        var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
        var instanceNameOption = new Option<string>("--instance-name") { Description = "Composed instance name", Required = true };
        var composedTemplateIdOption = new Option<int>("--composed-template-id") { Description = "Composed template ID", Required = true };

        var addCmd = new Command("add") { Description = "Add a composition to a template" };
        addCmd.Add(templateIdOption);
        addCmd.Add(instanceNameOption);
        addCmd.Add(composedTemplateIdOption);
        addCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new AddTemplateCompositionCommand(
                    result.GetValue(templateIdOption),
                    result.GetValue(instanceNameOption)!,
                    result.GetValue(composedTemplateIdOption)));
        });
        group.Add(addCmd);

        var deleteIdOption = new Option<int>("--id") { Description = "Composition ID", Required = true };
        var deleteCmd = new Command("delete") { Description = "Delete a template composition" };
        deleteCmd.Add(deleteIdOption);
        deleteCmd.SetAction(async (ParseResult result) =>
        {
            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                new DeleteTemplateCompositionCommand(result.GetValue(deleteIdOption)));
        });
        group.Add(deleteCmd);

        return group;
    }
}
