using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
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 readonly AdsClient _client = new();
    private readonly ConcurrentDictionary<uint, NotificationRegistration> _notifications = new();

    public AdsTwinCATClient()
    {
        _client.AdsNotificationEx += OnAdsNotificationEx;
    }

    public bool IsConnected => _client.IsConnected;

    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;
    }

    public async Task<(object? value, uint status)> ReadValueAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        CancellationToken cancellationToken)
    {
        try
        {
            var clrType = MapToClrType(type);
            var result = await _client.ReadValueAsync(symbolPath, clrType, cancellationToken)
                .ConfigureAwait(false);

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

            var value = result.Value;
            if (bitIndex is int bit && type == TwinCATDataType.Bool && value is not bool)
                value = ExtractBit(value, bit);

            return (value, TwinCATStatusMapper.Good);
        }
        catch (AdsErrorException ex)
        {
            return (null, TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode));
        }
    }

    public async Task<uint> WriteValueAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        object? value,
        CancellationToken cancellationToken)
    {
        if (bitIndex is int && type == TwinCATDataType.Bool)
            throw new NotSupportedException(
                "BOOL-within-word writes require read-modify-write; tracked in task #181.");

        try
        {
            var converted = ConvertForWrite(type, value);
            var result = await _client.WriteValueAsync(symbolPath, converted, cancellationToken)
                .ConfigureAwait(false);
            return result.ErrorCode == AdsErrorCode.NoError
                ? TwinCATStatusMapper.Good
                : TwinCATStatusMapper.MapAdsError((uint)result.ErrorCode);
        }
        catch (AdsErrorException ex)
        {
            return TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode);
        }
    }

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

    public async Task<ITwinCATNotificationHandle> AddNotificationAsync(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        TimeSpan cycleTime,
        Action<string, object?> onChange,
        CancellationToken cancellationToken)
    {
        var clrType = MapToClrType(type);
        // NotificationSettings takes cycle + max-delay in 100ns units. AdsTransMode.OnChange
        // fires when the value differs; OnCycle fires every cycle. OnChange is the right default
        // for OPC UA data-change semantics — the PLC already has the best view of "has this
        // changed" so we let it decide.
        var cycleTicks = (uint)Math.Max(1, cycleTime.Ticks / TimeSpan.TicksPerMillisecond * 10_000);
        var settings = new NotificationSettings(AdsTransMode.OnChange, (int)cycleTicks, 0);

        // 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;
    }

    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);
        try { reg.OnChange(reg.SymbolPath, value); } catch { /* consumer-side errors don't crash the ADS thread */ }
    }

    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 */ }
    }

    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.
        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

        foreach (ISymbol symbol in loader.Symbols)
        {
            if (cancellationToken.IsCancellationRequested) yield break;
            var mapped = MapSymbolTypeName(symbol.DataType?.Name);
            var readOnly = !IsSymbolWritable(symbol);
            yield return new TwinCATDiscoveredSymbol(symbol.InstancePath, mapped, readOnly);
        }
    }

    private static TwinCATDataType? MapSymbolTypeName(string? typeName) => typeName 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;
    }

    public void Dispose()
    {
        _client.AdsNotificationEx -= OnAdsNotificationEx;
        _notifications.Clear();
        _client.Dispose();
    }

    private sealed class NotificationRegistration(
        string symbolPath,
        TwinCATDataType type,
        int? bitIndex,
        Action<string, object?> onChange,
        AdsTwinCATClient owner,
        uint handle) : ITwinCATNotificationHandle
    {
        public string SymbolPath { get; } = symbolPath;
        public TwinCATDataType Type { get; } = type;
        public int? BitIndex { get; } = bitIndex;
        public Action<string, object?> OnChange { get; } = onChange;

        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
{
    public ITwinCATClient Create() => new AdsTwinCATClient();
}
