using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;

/// <summary>
///     Walk the target's symbol table (ADS <c>SymbolLoaderFactory</c>, flat mode) and print every
///     symbol the driver's atomic-type mapper recognizes. Same path <c>DiscoverAsync</c> takes
///     when <c>EnableControllerBrowse = true</c> — structured UDTs / function-block instances
///     won't appear because the driver filters to the supported primitive surface.
/// </summary>
/// <remarks>
///     Inherits from <see cref="TwinCATCommandBase"/> rather than
///     <see cref="TwinCATTagCommandBase"/> so the <c>--poll-only</c> flag does NOT surface in
///     <c>browse --help</c>: browse never subscribes, the flag would be a no-op, and the help
///     text would mislead users.
/// </remarks>
[Command("browse", Description = "Enumerate controller symbols via the driver's DiscoverAsync walk.")]
public sealed class BrowseCommand : TwinCATCommandBase
{
    /// <summary>Gets or sets the case-sensitive instance-path prefix to filter on.</summary>
    [CommandOption("prefix", Description =
        "Case-sensitive instance-path prefix to filter on (e.g. 'GVL_Fixture' or " +
        "'MAIN.'). Empty (default) prints everything.")]
    public string? Prefix { get; init; }

    /// <summary>Gets or sets the maximum number of symbols to print.</summary>
    [CommandOption("max", Description =
        "Maximum number of symbols to print. 0 = unbounded (default 500 for large " +
        "controllers — flat-mode symbol counts easily top 10k).")]
    public int Max { get; init; } = 500;

    /// <inheritdoc />
    public override async ValueTask ExecuteAsync(IConsole console)
    {
        Validate();
        ConfigureLogging();
        var ct = console.RegisterCancellationHandler();

        // Browse-only — no declared tags. EnableControllerBrowse=true flips DiscoverAsync's
        // symbol-walk on so every recognized primitive surfaces through the builder. Native
        // ADS notifications are irrelevant here (DiscoverAsync never subscribes); leave the
        // default on so the options record matches the production wiring.
        var options = new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Gateway, $"cli-{AmsNetId}:{AmsPort}")],
            RawTags = [],
            Timeout = Timeout,
            Probe = new TwinCATProbeOptions { Enabled = false },
            UseNativeNotifications = true,
            EnableControllerBrowse = true,
        };

        await using var driver = new TwinCATDriver(options, DriverInstanceId);
        var builder = new CollectingAddressSpaceBuilder();
        try
        {
            await driver.InitializeAsync("{}", ct);
            await driver.DiscoverAsync(builder, ct);
        }
        finally
        {
            await driver.ShutdownAsync(CancellationToken.None);
            FlushLogging();
        }

        var matched = FilterByPrefix(builder.Variables, Prefix);
        var printLimit = PrintLimit(matched.Count, Max);

        await console.Output.WriteLineAsync($"AMS:          {AmsNetId}:{AmsPort}");
        await console.Output.WriteLineAsync(
            $"Symbols:      {matched.Count} matched ({builder.Variables.Count} total), showing {printLimit}");
        await console.Output.WriteLineAsync();

        foreach (var v in matched.Take(printLimit))
        {
            await console.Output.WriteLineAsync($"  [{AccessTag(v.Info)}] {v.Info.DriverDataType,-8} {v.BrowseName}");
        }

        if (matched.Count > printLimit)
            await console.Output.WriteLineAsync(
                $"  … {matched.Count - printLimit} more — raise --max or tighten --prefix");
    }

    /// <summary>
    ///     Case-sensitive prefix filter. A null/empty prefix keeps everything; otherwise we
    ///     keep symbols whose browse name starts with <paramref name="prefix"/> under
    ///     <see cref="StringComparison.Ordinal"/> — TwinCAT identifiers are case-sensitive on
    ///     the wire, so a relaxed match would be misleading.
    /// </summary>
    /// <param name="source">The source collection to filter.</param>
    /// <param name="prefix">The prefix to filter on, or null to keep everything.</param>
    /// <returns>The subset of <paramref name="source"/> whose browse name starts with <paramref name="prefix"/>.</returns>
    internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix(
        IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix)
        => source
            .Where(v => string.IsNullOrEmpty(prefix) || v.BrowseName.StartsWith(prefix, StringComparison.Ordinal))
            .ToList();

    /// <summary>
    ///     Cap-to-max projection. <paramref name="max"/> &lt;= 0 means unbounded, otherwise the
    ///     min of <paramref name="matchedCount"/> and <paramref name="max"/>.
    /// </summary>
    /// <param name="matchedCount">The number of matched items.</param>
    /// <param name="max">The maximum number to show, or 0 for unbounded.</param>
    /// <returns>The number of items to print — <paramref name="matchedCount"/> if <paramref name="max"/>
    ///     is unbounded, otherwise the smaller of the two.</returns>
    internal static int PrintLimit(int matchedCount, int max)
        => max <= 0 ? matchedCount : Math.Min(max, matchedCount);

    /// <summary>
    ///     Coarse RO/RW label used in the browse output. <see cref="SecurityClassification.ViewOnly"/>
    ///     is the only classification that is unconditionally read-only; everything else can be
    ///     written from at least one ACL tier, so the CLI labels it RW. The real per-tier
    ///     authorization is enforced server-side.
    /// </summary>
    /// <param name="info">The attribute info to label.</param>
    /// <returns><c>"RO"</c> when the attribute is view-only; otherwise <c>"RW"</c>.</returns>
    internal static string AccessTag(DriverAttributeInfo info)
        => info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW";

    /// <summary>An address space builder that collects variables for enumeration.</summary>
    internal sealed class CollectingAddressSpaceBuilder : IAddressSpaceBuilder
    {
        /// <summary>Gets the collected variables.</summary>
        public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = [];

        /// <inheritdoc />
        public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;

        /// <inheritdoc />
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
        {
            Variables.Add((browseName, info));
            return new Handle(info.FullName);
        }

        /// <inheritdoc />
        public void AddProperty(string name, DriverDataType type, object? value) { }

        /// <summary>A variable handle that stores the full reference.</summary>
        private sealed class Handle(string fullRef) : IVariableHandle
        {
            /// <inheritdoc />
            public string FullReference => fullRef;
            /// <inheritdoc />
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }

        /// <summary>A null sink that ignores alarm condition transitions.</summary>
        private sealed class NullSink : IAlarmConditionSink
        {
            /// <inheritdoc />
            public void OnTransition(AlarmEventArgs args) { }
        }
    }
}
