using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using TwinCAT;
using TwinCAT.Ads;
using TwinCAT.Ads.TypeSystem;
using TwinCAT.TypeSystem;

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

/// <summary>
///     Default <see cref="ITwinCATClient"/> backed by Beckhoff's <see cref="AdsClient"/>.
///     One instance per AMS target; reused across reads / writes / probes.
/// </summary>
/// <remarks>
///     <para>Wire behavior depends on a reachable AMS router — on Windows the router comes
///     from TwinCAT XAR; elsewhere from the <c>Beckhoff.TwinCAT.Ads.TcpRouter</c> package
///     hosted by the server process. Neither is built-in here; deployment wires one in.</para>
///
///     <para>Error mapping — ADS error codes surface through <see cref="AdsErrorException"/>
///     and get translated to OPC UA status codes via <see cref="TwinCATStatusMapper.MapAdsError"/>.</para>
/// </remarks>
internal sealed class AdsTwinCATClient : ITwinCATClient
{
    private const int NotificationQueueCapacity = 50_000;

    private readonly AdsClient _client = new();
    private readonly ConcurrentDictionary<uint, NotificationRegistration> _notifications = new();

    // Marshals native ADS notifications off the AMS router thread onto a dedicated managed
    // task. The router-thread callback (OnAdsNotificationEx) only enqueues; DispatchLoopAsync
    // drains and invokes the per-tag OnChange delegates. Blocking the router thread would
    // stall every ADS notification across the whole process (docs/v2/driver-specs.md §6).
    private readonly Channel<PendingNotification> _notificationQueue =
        Channel.CreateBounded<PendingNotification>(new BoundedChannelOptions(NotificationQueueCapacity)
        {
            FullMode = BoundedChannelFullMode.DropWrite,
            SingleReader = true,
            SingleWriter = false,
        });
    private readonly CancellationTokenSource _dispatchCts = new();
    private readonly Task _dispatchLoop;
    private int _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="AdsTwinCATClient"/> class.
    /// </summary>
    public AdsTwinCATClient()
    {
        _client.AdsNotificationEx += OnAdsNotificationEx;
        _dispatchLoop = Task.Run(() => DispatchLoopAsync(_dispatchCts.Token));
    }

    private readonly record struct PendingNotification(NotificationRegistration Registration, object? Value);

    /// <inheritdoc />
    public bool IsConnected => _client.IsConnected;

    /// <summary>
    /// Occurs when the symbol version changes on the connected ADS target.
    /// </summary>
    public event EventHandler? OnSymbolVersionChanged;

    /// <summary>Raise <see cref="OnSymbolVersionChanged"/> when <paramref name="adsError"/> is <c>DeviceSymbolVersionInvalid</c> (1809 / 0x0711).</summary>
    private uint MapAndSignal(uint adsError)
    {
        if (TwinCATStatusMapper.IsSymbolVersionChanged(adsError))
            OnSymbolVersionChanged?.Invoke(this, EventArgs.Empty);
        return TwinCATStatusMapper.MapAdsError(adsError);
    }

    /// <inheritdoc />
    public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken)
    {
        if (_client.IsConnected) return Task.CompletedTask;
        _client.Timeout = (int)Math.Max(1_000, timeout.TotalMilliseconds);
        var netId = AmsNetId.Parse(address.NetId);
        _client.Connect(netId, address.Port);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task<(object? value, uint status)> ReadValueAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        int? arrayCount,
        CancellationToken cancellationToken)
    {
        try
        {
            // Bit-indexed BOOL — TwinCAT's symbol table doesn't expose "WordVar.N" as its
            // own symbolic entry (ADS returns DeviceSymbolNotFound), so we read the parent
            // container as its widest unsigned primitive and extract the bit locally. The
            // .N suffix added by TwinCATSymbolPath.ToAdsSymbolName needs to come back off
            // first. uint covers WORD / DWORD containers; BYTE-sized bit containers are
            // rare in real code and promoting to uint is harmless for them. Bit-within-word
            // is inherently scalar — arrayCount does not apply.
            if (bitIndex is int bit && type == TwinCATDataType.Bool)
            {
                var parent = StripBitSuffix(symbolPath);
                var parentResult = await _client.ReadValueAsync(parent, typeof(uint), cancellationToken)
                    .ConfigureAwait(false);
                if (parentResult.ErrorCode != AdsErrorCode.NoError)
                    return (null, MapAndSignal((uint)parentResult.ErrorCode));
                return (ExtractBit(parentResult.Value, bit), TwinCATStatusMapper.Good);
            }

            // Array read — ADS reads array symbols natively into a typed managed array. We ask
            // the SDK for the element-CLR-type's 1-D array (e.g. int[]); the .NET binding
            // marshals the whole array in one read. The returned value is already the boxed
            // element-typed array (int[] / float[] / bool[] / string[] / …), so the runtime's
            // OPC UA layer sees a proper 1-D array value. ASSUMPTION (no live ADS here):
            // AdsClient.ReadValueAsync(symbol, elementType.MakeArrayType(), ct) returns the
            // typed array — this matches Beckhoff's documented generic ReadValue<T[]> surface
            // (InfoSys tcadsnetref / SumRead samples). Verified only against the fake client.
            if (arrayCount is int count && count > 0)
            {
                var elementType = MapToClrType(type);
                var arrayType = elementType.MakeArrayType();
                var arrResult = await _client.ReadValueAsync(symbolPath, arrayType, cancellationToken)
                    .ConfigureAwait(false);
                if (arrResult.ErrorCode != AdsErrorCode.NoError)
                    return (null, MapAndSignal((uint)arrResult.ErrorCode));
                return (arrResult.Value, TwinCATStatusMapper.Good);
            }

            var clrType = MapToClrType(type);
            var result = await _client.ReadValueAsync(symbolPath, clrType, cancellationToken)
                .ConfigureAwait(false);

            if (result.ErrorCode != AdsErrorCode.NoError)
                return (null, MapAndSignal((uint)result.ErrorCode));

            return (result.Value, TwinCATStatusMapper.Good);
        }
        catch (AdsErrorException ex)
        {
            return (null, MapAndSignal((uint)ex.ErrorCode));
        }
    }

    private static string StripBitSuffix(string symbolPath)
    {
        var lastDot = symbolPath.LastIndexOf('.');
        if (lastDot < 0) return symbolPath;
        return int.TryParse(symbolPath.AsSpan(lastDot + 1), out _)
            ? symbolPath[..lastDot]
            : symbolPath;
    }

    /// <inheritdoc />
    public async Task<uint> WriteValueAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        object? value,
        CancellationToken cancellationToken)
    {
        try
        {
            var converted = ConvertForWrite(type, value);
            var result = await _client.WriteValueAsync(symbolPath, converted, cancellationToken)
                .ConfigureAwait(false);
            return result.ErrorCode == AdsErrorCode.NoError
                ? TwinCATStatusMapper.Good
                : MapAndSignal((uint)result.ErrorCode);
        }
        catch (AdsErrorException ex)
        {
            return MapAndSignal((uint)ex.ErrorCode);
        }
    }

    /// <inheritdoc />
    public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
    {
        try
        {
            var state = await _client.ReadStateAsync(cancellationToken).ConfigureAwait(false);
            return state.ErrorCode == AdsErrorCode.NoError;
        }
        catch
        {
            return false;
        }
    }

    /// <inheritdoc />
    public async Task<ITwinCATNotificationHandle> AddNotificationAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        TimeSpan cycleTime,
        int maxDelayMs,
        Action<string, object?> onChange,
        CancellationToken cancellationToken)
    {
        var clrType = MapToClrType(type);
        var cycleMs = (int)Math.Max(1, cycleTime.TotalMilliseconds);
        var settings = new NotificationSettings(AdsTransMode.OnChange, cycleMs, Math.Max(0, maxDelayMs));

        // AddDeviceNotificationExAsync returns Task<ResultHandle>; AdsNotificationEx fires
        // with the handle as part of the event args so we use the handle as the correlation
        // key into _notifications.
        var result = await _client.AddDeviceNotificationExAsync(
            symbolPath, settings, userData: null, clrType, args: null, cancellationToken)
            .ConfigureAwait(false);
        if (result.ErrorCode != AdsErrorCode.NoError)
            throw new InvalidOperationException(
                $"AddDeviceNotificationExAsync failed with ADS error {result.ErrorCode} for {symbolPath}");

        var reg = new NotificationRegistration(symbolPath, type, bitIndex, onChange, this, result.Handle);
        _notifications[result.Handle] = reg;
        return reg;
    }

    /// <summary>
    ///     Runs on the <see cref="AdsClient"/> AMS router thread. Does the cheap bit-extraction
    ///     decode then enqueues — no driver logic, no consumer callbacks — so a slow consumer
    ///     can never stall ADS notification delivery for the rest of the process.
    ///     Drops the notification (DropWrite) if the queue is saturated.
    /// </summary>
    private void OnAdsNotificationEx(object? sender, AdsNotificationExEventArgs args)
    {
        if (!_notifications.TryGetValue(args.Handle, out var reg)) return;
        var value = args.Value;
        if (reg.BitIndex is int bit && reg.Type == TwinCATDataType.Bool && value is not bool)
            value = ExtractBit(value, bit);
        _notificationQueue.Writer.TryWrite(new PendingNotification(reg, value));
    }

    private async Task DispatchLoopAsync(CancellationToken ct)
    {
        try
        {
            await foreach (var pending in _notificationQueue.Reader.ReadAllAsync(ct).ConfigureAwait(false))
            {
                try { pending.Registration.OnChange(pending.Registration.SymbolPath, pending.Value); }
                catch { /* consumer-side errors stay on this managed task, not the ADS thread */ }
            }
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Clean shutdown.
        }
    }

    /// <summary>
    /// Deletes a notification subscription by handle.
    /// </summary>
    /// <param name="handle">The notification handle to delete.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task representing the asynchronous delete operation.</returns>
    internal async Task DeleteNotificationAsync(uint handle, CancellationToken cancellationToken)
    {
        _notifications.TryRemove(handle, out _);
        try { await _client.DeleteDeviceNotificationAsync(handle, cancellationToken).ConfigureAwait(false); }
        catch { /* best-effort tear-down; target may already be gone */ }
    }

    /// <inheritdoc />
    public async IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        // SymbolLoaderFactory downloads the symbol-info blob once then iterates locally — the
        // async surface on this interface is for our callers, not for the underlying call which
        // is effectively sync on top of the already-open AdsClient.
        //
        // LIVE RISK (operator-gated, not yet verified against a real TC3 target): Flat mode
        // may not populate ISymbol.SubSymbols on struct/UDT/FB symbols on some TC3 firmware
        // versions — in that case TwinCATSymbolExpander sees no children and drops those
        // symbols silently. If struct members don't surface on a real target, switch to
        // SymbolsLoadMode.VirtualTree which is documented to always populate SubSymbols via
        // the hierarchy. Do NOT change the mode here until confirmed against live ADS — Flat is
        // required for Phase 4c array discovery to work correctly.
        var settings = new SymbolLoaderSettings(SymbolsLoadMode.Flat);
        var loader = SymbolLoaderFactory.Create(_client, settings);
        await Task.Yield(); // honors the async surface; pragmatic given the loader itself is sync

        // Expand struct/UDT/FB-instance symbols down to their addressable atomic members via the
        // pure TwinCATSymbolExpander (unit-proven; the real ISymbol surface is non-injectable so the
        // recursion is tested through the ITwinCATSymbolNode abstraction, then adapted here). The
        // expander dedups by InstancePath, so it also neutralizes any Flat-vs-tree double-listing.
        foreach (var ds in TwinCATSymbolExpander.ExpandLeaves(
                     loader.Symbols.Select(s => new AdsSymbolNode(s))))
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return ds;
        }
    }

    /// <summary>
    ///     Adapts Beckhoff's <see cref="ISymbol"/> onto <see cref="ITwinCATSymbolNode"/> so the pure
    ///     <see cref="TwinCATSymbolExpander"/> can walk it. Children are materialized eagerly per node
    ///     (the loader has already downloaded the whole symbol-info blob, so descending sub-symbols is
    ///     a local operation). Reuses the existing <see cref="MapSymbolType"/> + <see cref="IsSymbolWritable"/>
    ///     so atomic-type mapping and access-rights handling stay identical to the pre-expansion path.
    /// </summary>
    private sealed class AdsSymbolNode : ITwinCATSymbolNode
    {
        // Computed once in the ctor so repeated property access during recursive expansion
        // doesn't re-materialize Children or re-invoke MapSymbolType / IsSymbolWritable.
        private readonly ISymbol _symbol;
        private readonly bool _isStruct;
        private readonly (TwinCATDataType? Type, int? ArrayLength) _mapped;
        private readonly IReadOnlyList<ITwinCATSymbolNode> _children;
        private readonly bool _readOnly;

        /// <summary>
        /// Initializes a new instance of the <see cref="AdsSymbolNode"/> class, computing its
        /// struct/mapped-type/children/read-only state eagerly from the wrapped <paramref name="symbol"/>.
        /// </summary>
        /// <param name="symbol">The Beckhoff ADS symbol to adapt.</param>
        public AdsSymbolNode(ISymbol symbol)
        {
            _symbol = symbol;
            _isStruct = symbol.DataType?.Category == DataTypeCategory.Struct;
            _mapped = MapSymbolType(symbol.DataType);
            _children = symbol.SubSymbols is { } subs
                ? subs.Select(s => (ITwinCATSymbolNode)new AdsSymbolNode(s)).ToList()
                : [];
            _readOnly = !IsSymbolWritable(symbol);
        }

        /// <inheritdoc />
        public string InstancePath => _symbol.InstancePath;

        /// <inheritdoc />
        public bool IsStruct => _isStruct;

        /// <inheritdoc />
        public (TwinCATDataType? Type, int? ArrayLength) Mapped => _mapped;

        /// <inheritdoc />
        public IReadOnlyList<ITwinCATSymbolNode> Children => _children;

        /// <inheritdoc />
        public bool ReadOnly => _readOnly;
    }

    /// <summary>
    ///     Resolves a symbol's <see cref="IDataType"/> to the driver's atomic
    ///     <see cref="TwinCATDataType"/> plus an optional 1-D array length.
    ///     <para>
    ///     A 1-D array symbol (<c>Category == Array</c>, <see cref="IArrayType.Dimensions"/> count 1)
    ///     surfaces as its ELEMENT type with the dimension's <see cref="IDimension.ElementCount"/> as
    ///     the array length. ASSUMPTION (no live ADS here): the array <see cref="IDataType"/> exposes
    ///     <see cref="IArrayType"/> with <see cref="IArrayType.ElementType"/> + a
    ///     <see cref="IArrayType.Dimensions"/> collection whose single <see cref="IDimension"/> carries
    ///     <see cref="IDimension.ElementCount"/> — matches the TwinCAT TypeSystem surface
    ///     (<c>TwinCAT.TypeSystem.IArrayType</c> / <c>IDimensionCollection.GetDimensionLengths()</c>).
    ///     Verified by unit test against the fake client only.
    ///     </para>
    ///     Multi-dimensional arrays (Dimensions.Count > 1, including jagged) are NOT in scope for
    ///     Phase 4c — they fall through as a scalar/unsupported <c>null</c> (which DiscoverAsync drops),
    ///     never silently mis-reported as a 1-D array.
    /// </summary>
    private static (TwinCATDataType? type, int? arrayLength) MapSymbolType(IDataType? dataType)
    {
        if (dataType is null) return (null, null);

        if (dataType.Category == DataTypeCategory.Array && dataType is IArrayType arr)
        {
            // 1-D only. A multi-dim / jagged array is out of atomic scope → drop (null).
            var dims = arr.Dimensions;
            if (arr.IsJagged || dims is null || dims.Count != 1) return (null, null);

            var element = MapSymbolTypeName(arr.ElementType?.Name);
            if (element is null) return (null, null); // element type is itself unsupported (UDT array etc.)

            var length = dims[0].ElementCount;
            return length > 0 ? (element, length) : (null, null);
        }

        return (MapSymbolTypeName(dataType.Name), null);
    }

    private static TwinCATDataType? MapSymbolTypeName(string? typeName)
    {
        if (typeName is null) return null;
        // SymbolLoader emits STRING(80) / WSTRING(80) with the declared bound baked into
        // the type name — strip the "(...)" suffix so sized strings map onto the bare
        // String/WString atom the driver speaks.
        var paren = typeName.IndexOf('(');
        var bare = paren > 0 ? typeName[..paren] : typeName;
        return bare switch
        {
            "BOOL" or "BIT" => TwinCATDataType.Bool,
            "SINT" or "BYTE" => TwinCATDataType.SInt,
            "USINT" => TwinCATDataType.USInt,
            "INT" or "WORD" => TwinCATDataType.Int,
            "UINT" => TwinCATDataType.UInt,
            "DINT" or "DWORD" => TwinCATDataType.DInt,
            "UDINT" => TwinCATDataType.UDInt,
            "LINT" or "LWORD" => TwinCATDataType.LInt,
            "ULINT" => TwinCATDataType.ULInt,
            "REAL" => TwinCATDataType.Real,
            "LREAL" => TwinCATDataType.LReal,
            "STRING" => TwinCATDataType.String,
            "WSTRING" => TwinCATDataType.WString,
            "TIME" => TwinCATDataType.Time,
            "DATE" => TwinCATDataType.Date,
            "DT" or "DATE_AND_TIME" => TwinCATDataType.DateTime,
            "TOD" or "TIME_OF_DAY" => TwinCATDataType.TimeOfDay,
            _ => null, // UDTs / FB instances / arrays / pointers — out of atomic scope
        };
    }

    private static bool IsSymbolWritable(ISymbol symbol)
    {
        // SymbolAccessRights is a flags enum — the Write bit indicates a writable symbol.
        // When the symbol implementation doesn't surface it, assume writable + let the PLC
        // return AccessDenied at write time.
        if (symbol is Symbol s) return (s.AccessRights & SymbolAccessRights.Write) != 0;
        return true;
    }

    /// <summary>
    /// Disposes the client and releases all resources.
    /// </summary>
    public void Dispose()
    {
        if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
        _client.AdsNotificationEx -= OnAdsNotificationEx;
        _notifications.Clear();

        // Stop the dispatch loop: complete the queue so the reader drains + exits, then
        // cancel as a backstop. Best-effort wait so a wedged consumer can't hang teardown.
        _notificationQueue.Writer.TryComplete();
        _dispatchCts.Cancel();
        try { _dispatchLoop.Wait(TimeSpan.FromSeconds(2)); } catch { /* shutdown */ }
        _dispatchCts.Dispose();

        _client.Dispose();
    }

    private sealed class NotificationRegistration(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        Action<string, object?> onChange,
        AdsTwinCATClient owner,
        uint handle) : ITwinCATNotificationHandle
    {
        /// <summary>
        /// Gets the symbol path being monitored.
        /// </summary>
        public string SymbolPath { get; } = symbolPath;

        /// <summary>
        /// Gets the TwinCAT data type of the symbol.
        /// </summary>
        public TwinCATDataType Type { get; } = type;

        /// <summary>
        /// Gets the optional bit index for BOOL values.
        /// </summary>
        public int? BitIndex { get; } = bitIndex;

        /// <summary>
        /// Gets the callback to invoke when the value changes.
        /// </summary>
        public Action<string, object?> OnChange { get; } = onChange;

        /// <summary>
        /// Disposes the notification subscription.
        /// </summary>
        public void Dispose()
        {
            // Fire-and-forget AMS call — caller has already committed to the tear-down.
            _ = owner.DeleteNotificationAsync(handle, CancellationToken.None);
        }
    }

    private static Type MapToClrType(TwinCATDataType type) => type switch
    {
        TwinCATDataType.Bool => typeof(bool),
        TwinCATDataType.SInt => typeof(sbyte),
        TwinCATDataType.USInt => typeof(byte),
        TwinCATDataType.Int => typeof(short),
        TwinCATDataType.UInt => typeof(ushort),
        TwinCATDataType.DInt => typeof(int),
        TwinCATDataType.UDInt => typeof(uint),
        TwinCATDataType.LInt => typeof(long),
        TwinCATDataType.ULInt => typeof(ulong),
        TwinCATDataType.Real => typeof(float),
        TwinCATDataType.LReal => typeof(double),
        TwinCATDataType.String or TwinCATDataType.WString => typeof(string),
        TwinCATDataType.Time or TwinCATDataType.Date
            or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => typeof(uint),
        _ => typeof(int),
    };

    private static object ConvertForWrite(TwinCATDataType type, object? value) => type switch
    {
        TwinCATDataType.Bool => Convert.ToBoolean(value),
        TwinCATDataType.SInt => Convert.ToSByte(value),
        TwinCATDataType.USInt => Convert.ToByte(value),
        TwinCATDataType.Int => Convert.ToInt16(value),
        TwinCATDataType.UInt => Convert.ToUInt16(value),
        TwinCATDataType.DInt => Convert.ToInt32(value),
        TwinCATDataType.UDInt => Convert.ToUInt32(value),
        TwinCATDataType.LInt => Convert.ToInt64(value),
        TwinCATDataType.ULInt => Convert.ToUInt64(value),
        TwinCATDataType.Real => Convert.ToSingle(value),
        TwinCATDataType.LReal => Convert.ToDouble(value),
        TwinCATDataType.String or TwinCATDataType.WString => Convert.ToString(value) ?? string.Empty,
        TwinCATDataType.Time or TwinCATDataType.Date
            or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => Convert.ToUInt32(value),
        _ => throw new NotSupportedException($"TwinCATDataType {type} not writable."),
    };

    private static bool ExtractBit(object? rawWord, int bit) => rawWord switch
    {
        short s => (s & (1 << bit)) != 0,
        ushort us => (us & (1 << bit)) != 0,
        int i => (i & (1 << bit)) != 0,
        uint ui => (ui & (1u << bit)) != 0,
        long l => (l & (1L << bit)) != 0,
        ulong ul => (ul & (1UL << bit)) != 0,
        _ => false,
    };
}

/// <summary>Default <see cref="ITwinCATClientFactory"/> — one <see cref="AdsTwinCATClient"/> per call.</summary>
internal sealed class AdsTwinCATClientFactory : ITwinCATClientFactory
{
    /// <inheritdoc />
    public ITwinCATClient Create() => new AdsTwinCATClient();
}
