using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;

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

/// <summary>
///     AB Legacy / PCCC driver — SLC 500, MicroLogix, PLC-5, LogixPccc. Implements
///     <see cref="IDriver"/> only at PR 1 time; read / write / discovery / subscribe / probe /
///     host-resolver capabilities ship in PRs 2 and 3.
/// </summary>
public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
    IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable
{
    private readonly AbLegacyDriverOptions _options;
    private readonly string _driverInstanceId;
    private readonly IAbLegacyTagFactory _tagFactory;
    private readonly PollGroupEngine _poll;
    private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, AbLegacyTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
    private DriverHealth _health = new(DriverState.Unknown, null, null);

    public event EventHandler<DataChangeEventArgs>? OnDataChange;
    public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;

    public AbLegacyDriver(AbLegacyDriverOptions options, string driverInstanceId,
        IAbLegacyTagFactory? tagFactory = null)
    {
        ArgumentNullException.ThrowIfNull(options);
        _options = options;
        _driverInstanceId = driverInstanceId;
        _tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
        _poll = new PollGroupEngine(
            reader: ReadAsync,
            onChange: (handle, tagRef, snapshot) =>
                OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
    }

    public string DriverInstanceId => _driverInstanceId;
    public string DriverType => "AbLegacy";

    public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
    {
        _health = new DriverHealth(DriverState.Initializing, null, null);
        try
        {
            foreach (var device in _options.Devices)
            {
                var addr = AbLegacyHostAddress.TryParse(device.HostAddress)
                    ?? throw new InvalidOperationException(
                        $"AbLegacy device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
                var profile = AbLegacyPlcFamilyProfile.ForFamily(device.PlcFamily);
                _devices[device.HostAddress] = new DeviceState(addr, device, profile);
            }
            foreach (var tag in _options.Tags) _tagsByName[tag.Name] = tag;

            // Probe loops — one per device when enabled + probe address configured.
            if (_options.Probe.Enabled && !string.IsNullOrWhiteSpace(_options.Probe.ProbeAddress))
            {
                foreach (var state in _devices.Values)
                {
                    state.ProbeCts = new CancellationTokenSource();
                    var ct = state.ProbeCts.Token;
                    _ = Task.Run(() => ProbeLoopAsync(state, ct), ct);
                }
            }
            _health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
        }
        catch (Exception ex)
        {
            _health = new DriverHealth(DriverState.Faulted, null, ex.Message);
            throw;
        }
        return Task.CompletedTask;
    }

    public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
    {
        await ShutdownAsync(cancellationToken).ConfigureAwait(false);
        await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
    }

    public async Task ShutdownAsync(CancellationToken cancellationToken)
    {
        await _poll.DisposeAsync().ConfigureAwait(false);
        foreach (var state in _devices.Values)
        {
            try { state.ProbeCts?.Cancel(); } catch { }
            state.ProbeCts?.Dispose();
            state.ProbeCts = null;
            state.DisposeRuntimes();
        }
        _devices.Clear();
        _tagsByName.Clear();
        _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
    }

    public DriverHealth GetHealth() => _health;
    public long GetMemoryFootprint() => 0;
    public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    internal int DeviceCount => _devices.Count;
    internal DeviceState? GetDeviceState(string hostAddress) =>
        _devices.TryGetValue(hostAddress, out var s) ? s : null;

    // ---- IReadable ----

    public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
        IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(fullReferences);
        var now = DateTime.UtcNow;
        var results = new DataValueSnapshot[fullReferences.Count];

        for (var i = 0; i < fullReferences.Count; i++)
        {
            var reference = fullReferences[i];
            if (!_tagsByName.TryGetValue(reference, out var def))
            {
                results[i] = new DataValueSnapshot(null, AbLegacyStatusMapper.BadNodeIdUnknown, null, now);
                continue;
            }
            if (!_devices.TryGetValue(def.DeviceHostAddress, out var device))
            {
                results[i] = new DataValueSnapshot(null, AbLegacyStatusMapper.BadNodeIdUnknown, null, now);
                continue;
            }

            try
            {
                var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false);
                await runtime.ReadAsync(cancellationToken).ConfigureAwait(false);

                var status = runtime.GetStatus();
                if (status != 0)
                {
                    results[i] = new DataValueSnapshot(null,
                        AbLegacyStatusMapper.MapLibplctagStatus(status), null, now);
                    _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
                        $"libplctag status {status} reading {reference}");
                    continue;
                }

                var parsed = AbLegacyAddress.TryParse(def.Address);
                var value = runtime.DecodeValue(def.DataType, parsed?.BitIndex);
                results[i] = new DataValueSnapshot(value, AbLegacyStatusMapper.Good, now, now);
                _health = new DriverHealth(DriverState.Healthy, now, null);
            }
            catch (OperationCanceledException) { throw; }
            catch (Exception ex)
            {
                results[i] = new DataValueSnapshot(null,
                    AbLegacyStatusMapper.BadCommunicationError, null, now);
                _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
            }
        }

        return results;
    }

    // ---- IWritable ----

    public async Task<IReadOnlyList<WriteResult>> WriteAsync(
        IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(writes);
        var results = new WriteResult[writes.Count];

        for (var i = 0; i < writes.Count; i++)
        {
            var w = writes[i];
            if (!_tagsByName.TryGetValue(w.FullReference, out var def))
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadNodeIdUnknown);
                continue;
            }
            if (!def.Writable)
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadNotWritable);
                continue;
            }
            if (!_devices.TryGetValue(def.DeviceHostAddress, out var device))
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadNodeIdUnknown);
                continue;
            }

            try
            {
                var parsed = AbLegacyAddress.TryParse(def.Address);

                // PCCC bit-within-word writes — task #181 pass 2. RMW against a parallel
                // parent-word runtime (strip the /N bit suffix). Per-parent-word lock serialises
                // concurrent bit writers. Applies to N-file bit-in-word (N7:0/3) + B-file bits
                // (B3:0/0). T/C/R sub-elements don't hit this path because they're not Bit typed.
                if (def.DataType == AbLegacyDataType.Bit && parsed?.BitIndex is int bit
                    && parsed.FileLetter is not "B" and not "I" and not "O")
                {
                    results[i] = new WriteResult(
                        await WriteBitInWordAsync(device, parsed, bit, w.Value, cancellationToken).ConfigureAwait(false));
                    continue;
                }

                var runtime = await EnsureTagRuntimeAsync(device, def, cancellationToken).ConfigureAwait(false);
                runtime.EncodeValue(def.DataType, parsed?.BitIndex, w.Value);
                await runtime.WriteAsync(cancellationToken).ConfigureAwait(false);

                var status = runtime.GetStatus();
                results[i] = new WriteResult(status == 0
                    ? AbLegacyStatusMapper.Good
                    : AbLegacyStatusMapper.MapLibplctagStatus(status));
            }
            catch (OperationCanceledException) { throw; }
            catch (NotSupportedException nse)
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadNotSupported);
                _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, nse.Message);
            }
            catch (Exception ex) when (ex is FormatException or InvalidCastException)
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadTypeMismatch);
            }
            catch (OverflowException)
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadOutOfRange);
            }
            catch (Exception ex)
            {
                results[i] = new WriteResult(AbLegacyStatusMapper.BadCommunicationError);
                _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
            }
        }

        return results;
    }

    // ---- ITagDiscovery ----

    public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(builder);
        var root = builder.Folder("AbLegacy", "AbLegacy");
        foreach (var device in _options.Devices)
        {
            var label = device.DeviceName ?? device.HostAddress;
            var deviceFolder = root.Folder(device.HostAddress, label);
            var tagsForDevice = _options.Tags.Where(t =>
                string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase));
            foreach (var tag in tagsForDevice)
            {
                deviceFolder.Variable(tag.Name, tag.Name, new DriverAttributeInfo(
                    FullName: tag.Name,
                    DriverDataType: tag.DataType.ToDriverDataType(),
                    IsArray: false,
                    ArrayDim: null,
                    SecurityClass: tag.Writable
                        ? SecurityClassification.Operate
                        : SecurityClassification.ViewOnly,
                    IsHistorized: false,
                    IsAlarm: false,
                    WriteIdempotent: tag.WriteIdempotent));
            }
        }
        return Task.CompletedTask;
    }

    // ---- ISubscribable (polling overlay via shared engine) ----

    public Task<ISubscriptionHandle> SubscribeAsync(
        IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
        Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval));

    public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
    {
        _poll.Unsubscribe(handle);
        return Task.CompletedTask;
    }

    // ---- IHostConnectivityProbe ----

    public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
        [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];

    private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct)
    {
        var probeParams = new AbLegacyTagCreateParams(
            Gateway: state.ParsedAddress.Gateway,
            Port: state.ParsedAddress.Port,
            CipPath: state.ParsedAddress.CipPath,
            LibplctagPlcAttribute: state.Profile.LibplctagPlcAttribute,
            TagName: _options.Probe.ProbeAddress!,
            Timeout: _options.Probe.Timeout);

        IAbLegacyTagRuntime? probeRuntime = null;
        while (!ct.IsCancellationRequested)
        {
            var success = false;
            try
            {
                probeRuntime ??= _tagFactory.Create(probeParams);
                if (!state.ProbeInitialized)
                {
                    await probeRuntime.InitializeAsync(ct).ConfigureAwait(false);
                    state.ProbeInitialized = true;
                }
                await probeRuntime.ReadAsync(ct).ConfigureAwait(false);
                success = probeRuntime.GetStatus() == 0;
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
            catch
            {
                try { probeRuntime?.Dispose(); } catch { }
                probeRuntime = null;
                state.ProbeInitialized = false;
            }

            TransitionDeviceState(state, success ? HostState.Running : HostState.Stopped);

            try { await Task.Delay(_options.Probe.Interval, ct).ConfigureAwait(false); }
            catch (OperationCanceledException) { break; }
        }
        try { probeRuntime?.Dispose(); } catch { }
    }

    private void TransitionDeviceState(DeviceState state, HostState newState)
    {
        HostState old;
        lock (state.ProbeLock)
        {
            old = state.HostState;
            if (old == newState) return;
            state.HostState = newState;
            state.HostStateChangedUtc = DateTime.UtcNow;
        }
        OnHostStatusChanged?.Invoke(this,
            new HostStatusChangedEventArgs(state.Options.HostAddress, old, newState));
    }

    // ---- IPerCallHostResolver ----

    public string ResolveHost(string fullReference)
    {
        if (_tagsByName.TryGetValue(fullReference, out var def))
            return def.DeviceHostAddress;
        return _options.Devices.FirstOrDefault()?.HostAddress ?? DriverInstanceId;
    }

    /// <summary>
    ///     Read-modify-write one bit within a PCCC N-file word. Strips the /N bit suffix to
    ///     form the parent-word address (N7:0/3 → N7:0), creates / reuses a parent-word runtime
    ///     typed as Int16, serialises concurrent bit writers against the same parent via a
    ///     per-parent <see cref="SemaphoreSlim"/>.
    /// </summary>
    private async Task<uint> WriteBitInWordAsync(
        AbLegacyDriver.DeviceState device, AbLegacyAddress bitAddress, int bit, object? value, CancellationToken ct)
    {
        var parentAddress = bitAddress with { BitIndex = null };
        var parentName = parentAddress.ToLibplctagName();

        var rmwLock = device.GetRmwLock(parentName);
        await rmwLock.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            var parentRuntime = await EnsureParentRuntimeAsync(device, parentName, ct).ConfigureAwait(false);
            await parentRuntime.ReadAsync(ct).ConfigureAwait(false);
            var readStatus = parentRuntime.GetStatus();
            if (readStatus != 0) return AbLegacyStatusMapper.MapLibplctagStatus(readStatus);

            var current = Convert.ToInt32(parentRuntime.DecodeValue(AbLegacyDataType.Int, bitIndex: null) ?? 0);
            var updated = Convert.ToBoolean(value)
                ? current | (1 << bit)
                : current & ~(1 << bit);

            parentRuntime.EncodeValue(AbLegacyDataType.Int, bitIndex: null, (short)updated);
            await parentRuntime.WriteAsync(ct).ConfigureAwait(false);
            var writeStatus = parentRuntime.GetStatus();
            return writeStatus == 0
                ? AbLegacyStatusMapper.Good
                : AbLegacyStatusMapper.MapLibplctagStatus(writeStatus);
        }
        finally
        {
            rmwLock.Release();
        }
    }

    private async Task<IAbLegacyTagRuntime> EnsureParentRuntimeAsync(
        AbLegacyDriver.DeviceState device, string parentName, CancellationToken ct)
    {
        if (device.ParentRuntimes.TryGetValue(parentName, out var existing)) return existing;

        var runtime = _tagFactory.Create(new AbLegacyTagCreateParams(
            Gateway: device.ParsedAddress.Gateway,
            Port: device.ParsedAddress.Port,
            CipPath: device.ParsedAddress.CipPath,
            LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
            TagName: parentName,
            Timeout: _options.Timeout));
        try
        {
            await runtime.InitializeAsync(ct).ConfigureAwait(false);
        }
        catch
        {
            runtime.Dispose();
            throw;
        }
        device.ParentRuntimes[parentName] = runtime;
        return runtime;
    }

    private async Task<IAbLegacyTagRuntime> EnsureTagRuntimeAsync(
        DeviceState device, AbLegacyTagDefinition def, CancellationToken ct)
    {
        if (device.Runtimes.TryGetValue(def.Name, out var existing)) return existing;

        var parsed = AbLegacyAddress.TryParse(def.Address)
            ?? throw new InvalidOperationException(
                $"AbLegacy tag '{def.Name}' has malformed Address '{def.Address}'.");

        var runtime = _tagFactory.Create(new AbLegacyTagCreateParams(
            Gateway: device.ParsedAddress.Gateway,
            Port: device.ParsedAddress.Port,
            CipPath: device.ParsedAddress.CipPath,
            LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
            TagName: parsed.ToLibplctagName(),
            Timeout: _options.Timeout));
        try
        {
            await runtime.InitializeAsync(ct).ConfigureAwait(false);
        }
        catch
        {
            runtime.Dispose();
            throw;
        }
        device.Runtimes[def.Name] = runtime;
        return runtime;
    }

    public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
    public async ValueTask DisposeAsync() => await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);

    internal sealed class DeviceState(
        AbLegacyHostAddress parsedAddress,
        AbLegacyDeviceOptions options,
        AbLegacyPlcFamilyProfile profile)
    {
        public AbLegacyHostAddress ParsedAddress { get; } = parsedAddress;
        public AbLegacyDeviceOptions Options { get; } = options;
        public AbLegacyPlcFamilyProfile Profile { get; } = profile;
        public Dictionary<string, IAbLegacyTagRuntime> Runtimes { get; } =
            new(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        ///     Parent-word runtimes for bit-within-word RMW writes (task #181). Keyed by the
        ///     parent address (bit suffix stripped) — e.g. writes to N7:0/3 + N7:0/5 share a
        ///     single parent runtime for N7:0.
        /// </summary>
        public Dictionary<string, IAbLegacyTagRuntime> ParentRuntimes { get; } =
            new(StringComparer.OrdinalIgnoreCase);

        private readonly System.Collections.Concurrent.ConcurrentDictionary<string, SemaphoreSlim> _rmwLocks = new();

        public SemaphoreSlim GetRmwLock(string parentName) =>
            _rmwLocks.GetOrAdd(parentName, _ => new SemaphoreSlim(1, 1));

        public object ProbeLock { get; } = new();
        public HostState HostState { get; set; } = HostState.Unknown;
        public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
        public CancellationTokenSource? ProbeCts { get; set; }
        public bool ProbeInitialized { get; set; }

        public void DisposeRuntimes()
        {
            foreach (var r in Runtimes.Values) r.Dispose();
            Runtimes.Clear();
            foreach (var r in ParentRuntimes.Values) r.Dispose();
            ParentRuntimes.Clear();
        }
    }
}
