using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Aidlab.BLE;
using UnityEngine;
using UnityEngine.Events;
using Stopwatch = System.Diagnostics.Stopwatch;

namespace Aidlab
{
    public class AidlabSDK : MonoBehaviour
    {
        private static bool initAttempted;

        public event Action Connected;
        public event Action Disconnected;
        public event Action<AidlabError> ErrorReceived;
        public event Action<string, byte[], ulong> PayloadReceived;
        public event Action<string, int, byte[], ulong> ProcessErrorReceived;

        public static void init()
        {
            init(null);
        }

	        public static void init(string deviceName)
	        {
            if (FindAnyObjectByType<AidlabSDK>() != null)
                return;

            if (initAttempted)
                return;

            initAttempted = true;

            var sdkObject = new GameObject("AidlabSDK");
            DontDestroyOnLoad(sdkObject);
            var instance = sdkObject.AddComponent<AidlabSDK>();
            if (!string.IsNullOrEmpty(deviceName))
            {
                instance.deviceNameToConnect = deviceName;
            }
	        }

	        [SerializeField] private string deviceNameToConnect = "Aidlab";
	        [SerializeField] private Signal[] liveSignalsToCollect = { Signal.Ecg, Signal.Temperature, Signal.HeartRate, Signal.Rr };
	        [SerializeField] private Signal[] syncSignalsToStore = new Signal[0];

        private readonly MainThreadWorker mainThreadWorker = new MainThreadWorker();
        public static readonly AidlabDelegate aidlabDelegate = new AidlabDelegate();
        public static readonly AidlabSyncDelegate aidlabSyncDelegate = new AidlabSyncDelegate();

        private IBleConnector bleConnector;
#if UNITY_ANDROID && !UNITY_EDITOR
        private AndroidAidlabBackend androidBackend;
#endif
        public string firmwareRevisionStr = "";
        public string hardwareRevisionStr = "";

        private IntPtr sdk = IntPtr.Zero;
        private GCHandle contextHandle;

        private readonly object txLock = new object();
        private readonly Queue<byte[]> pendingFrames = new Queue<byte[]>();
        private readonly object frameConfirmationLock = new object();
        private Timer frameConfirmationTimer;
        private long frameConfirmationGeneration;
        private bool awaitingFrameConfirmation;
        private TaskCompletionSource<bool> frameConfirmationCompletion;
        private int expectedFrameCallbackThreadId = -1;
        private byte[] trackedOutboundFrame;
        private readonly object commandLock = new object();
        private readonly SemaphoreSlim commandSemaphore = new SemaphoreSlim(1, 1);
        private sealed class PendingProcessCommand
        {
            public readonly TaskCompletionSource<int?> Response = new TaskCompletionSource<int?>();
            public readonly int? SpawnedProcessId;
            public readonly TaskCompletionSource<int?> SpawnedProcess;
            public bool ResponseReceived;

            public PendingProcessCommand(int? spawnedProcessId)
            {
                SpawnedProcessId = spawnedProcessId;
                SpawnedProcess = spawnedProcessId.HasValue ? new TaskCompletionSource<int?>() : null;
            }
        }

        private PendingProcessCommand pendingCommand;
        private TaskCompletionSource<int?> pendingProcessTermination;
        private int pendingTerminationPid = -1;
        private readonly Dictionary<int, int> activeProcessPids = new Dictionary<int, int>();
        private bool legacyCollectionEnabled;
        private const int SystemCreateSuccess = 0;
        private const int SystemCreateFailure = 1;
        private const int SystemKillSuccess = 2;
        private const int SystemKillFailure = 3;
        private const int SyncProcessId = 7;
        private const int FrameConfirmationTimeoutMs = 3000;
        private const string FastSyncMinimumFirmware = "3.7.83";

        private readonly object earlyRxLock = new object();
        private readonly Queue<byte[]> earlyRxChunks = new Queue<byte[]>();
        private int earlyRxBytesBuffered;
        private const int EarlyRxMaxBytes = 256 * 1024;

        private float lastDataReceivedTime = -1.0f;
        private bool receivedData = false;

        [Header("Diagnostics")]
        [SerializeField] private bool enablePacketTrace = false;

        private long debugRxPackets;
        private long debugRxBytes;
        private static long debugSignalUpdates;
        private long debugLastRxAtMs = -1;
        private long debugHrCallbacks;
        private long debugRrCallbacks;
        private long debugWearStateCallbacks;
        private long debugLastHrAtMs = -1;
        private long debugLastRrAtMs = -1;
        private long debugLastWearStateAtMs = -1;
        private int debugLastRxSize;
        private long debugCollectSentAtMs = -1;
        private long debugLastNoSignalLogAtMs = -1;

        public bool IsSdkInitialized
        {
            get
            {
#if UNITY_ANDROID && !UNITY_EDITOR
                return androidBackend?.IsConnected == true;
#else
                return sdk != IntPtr.Zero;
#endif
            }
        }
	        public float SecondsSinceLastPacket => lastDataReceivedTime < 0.0f ? -1.0f : (Time.time - lastDataReceivedTime);
	        public string TargetDeviceName => deviceNameToConnect ?? "";

	        public long DebugHrCallbacks => Interlocked.Read(ref debugHrCallbacks);
	        public long DebugRrCallbacks => Interlocked.Read(ref debugRrCallbacks);
	        public long DebugWearStateCallbacks => Interlocked.Read(ref debugWearStateCallbacks);
	        public long DebugLastHrAtMs => Interlocked.Read(ref debugLastHrAtMs);
	        public long DebugLastRrAtMs => Interlocked.Read(ref debugLastRrAtMs);
	        public long DebugLastWearStateAtMs => Interlocked.Read(ref debugLastWearStateAtMs);

	        public Signal[] LiveSignalsToCollect => liveSignalsToCollect;
	        public Signal[] SyncSignalsToStore => syncSignalsToStore;

	        public void SetCollectSignals(Signal[] liveSignals, Signal[] syncSignals)
	        {
	            liveSignalsToCollect = liveSignals ?? Array.Empty<Signal>();
	            syncSignalsToStore = syncSignals ?? Array.Empty<Signal>();

	            if (IsSdkInitialized && !string.IsNullOrEmpty(firmwareRevisionStr))
	            {
	                StartCollectAll();
	            }
	        }

        public void SetTargetDeviceName(string value)
        {
            string next = value ?? "";
            if (next == deviceNameToConnect)
                return;

            if (enablePacketTrace)
                Debug.Log($"AidlabSDK: switching target device name '{deviceNameToConnect}' -> '{next}'");
            deviceNameToConnect = next;

#if UNITY_ANDROID && !UNITY_EDITOR
            androidBackend?.SetTargetDeviceName(next);
#else
            bleConnector?.Disconnect();
            bleConnector = null;
#endif

            if (sdk != IntPtr.Zero)
            {
                DestroySdk();
            }

            firmwareRevisionStr = "";
            hardwareRevisionStr = "";
            lastDataReceivedTime = -1.0f;
            receivedData = false;
            debugRxPackets = 0;
            debugRxBytes = 0;
            Interlocked.Exchange(ref debugSignalUpdates, 0);
            debugCollectSentAtMs = -1;
            debugLastNoSignalLogAtMs = -1;
            lock (earlyRxLock)
            {
                earlyRxChunks.Clear();
                earlyRxBytesBuffered = 0;
            }
        }

        public long DebugRxPackets => Interlocked.Read(ref debugRxPackets);
        public long DebugRxBytes => Interlocked.Read(ref debugRxBytes);
        public long DebugSignalUpdates => Interlocked.Read(ref debugSignalUpdates);
        public long DebugLastRxAtMs => Interlocked.Read(ref debugLastRxAtMs);
        public int DebugLastRxSize => Volatile.Read(ref debugLastRxSize);

        private static readonly double StopwatchTicksToMs = 1000.0 / Stopwatch.Frequency;

        public static long DebugNowMs()
        {
            // Unity can target older .NET profiles where Environment.TickCount64 is unavailable.
            // Stopwatch timestamp is monotonic and perfect for relative timing.
            return (long)(Stopwatch.GetTimestamp() * StopwatchTicksToMs);
        }

        public string TransportStatus
        {
            get
            {
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
                if (bleConnector is BLEConnector win)
                    return win.CurrentStatus.ToString();
                return "Unknown";
#elif UNITY_ANDROID && !UNITY_EDITOR
                return androidBackend?.Status ?? "Starting";
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
                if (bleConnector is MacBLEConnector mac)
                    return mac.IsConnected ? "Connected" : "Scanning";
                return "Unknown";
#else
                return "Unsupported";
#endif
            }
        }

        public string TransportLastError
        {
            get
            {
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
                if (bleConnector is BLEConnector win)
                    return win.LastError;
                return "";
#elif UNITY_ANDROID && !UNITY_EDITOR
                return androidBackend?.LastError ?? "";
#else
                return "";
#endif
            }
        }

        public List<BLEApi.DeviceUpdate> GetKnownDevicesSnapshot()
        {
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
            if (bleConnector is BLEConnector win)
                return win.GetKnownDevicesSnapshot();
#endif
            return new List<BLEApi.DeviceUpdate>();
        }

        private static readonly AidlabAPI.callbackSampleTime EcgCallback = OnEcg;
        private static readonly AidlabAPI.callbackSampleTime RespirationCallback = OnRespiration;
        private static readonly AidlabAPI.callbackSampleTime TemperatureCallback = OnTemperature;
        private static readonly AidlabAPI.callbackAccelerometer AccelerometerCallback = OnAccelerometer;
        private static readonly AidlabAPI.callbackGyroscope GyroscopeCallback = OnGyroscope;
        private static readonly AidlabAPI.callbackMagnetometer MagnetometerCallback = OnMagnetometer;
        private static readonly AidlabAPI.callbackBatteryLevel BatteryCallback = OnBattery;
        private static readonly AidlabAPI.callbackActivity ActivityCallback = OnActivity;
        private static readonly AidlabAPI.callbackSteps StepsCallback = OnSteps;
        private static readonly AidlabAPI.callbackOrientation OrientationCallback = OnOrientation;
        private static readonly AidlabAPI.callbackQuaternion QuaternionCallback = OnQuaternion;
        private static readonly AidlabAPI.callbackRespirationRate RespirationRateCallback = OnRespirationRate;
        private static readonly AidlabAPI.callbackWearState WearStateCallback = OnWearState;
        private static readonly AidlabAPI.callbackHeartRate HeartRateCallback = OnHeartRate;
        private static readonly AidlabAPI.callbackRr RrCallback = OnRr;
        private static readonly AidlabAPI.callbackSoundVolume SoundVolumeCallback = OnSoundVolume;
        private static readonly AidlabAPI.callbackExercise ExerciseCallback = OnExercise;
        private static readonly AidlabAPI.callbackUserEvent UserEventCallback = OnUserEvent;
        private static readonly AidlabAPI.callbackPressure PressureCallback = OnPressure;
        private static readonly AidlabAPI.callbackWearState PressureWearStateCallback = OnPressureWearState;
        private static readonly AidlabAPI.callbackBodyPosition BodyPositionCallback = OnBodyPosition;
        private static readonly AidlabAPI.callbackSignalQuality SignalQualityCallback = OnSignalQuality;
        private static readonly AidlabAPI.callbackEda EdaCallback = OnEda;
        private static readonly AidlabAPI.callbackGps GpsCallback = OnGps;

        private static readonly AidlabAPI.callbackSyncState SyncStateCallback = OnSyncState;
        private static readonly AidlabAPI.callbackUnsynchronizedSize UnsynchronizedSizeCallback = OnUnsynchronizedSize;
        private static readonly AidlabAPI.callbackSampleTime PastEcgCallback = OnPastEcg;
        private static readonly AidlabAPI.callbackSampleTime PastRespirationCallback = OnPastRespiration;
        private static readonly AidlabAPI.callbackSampleTime PastTemperatureCallback = OnPastTemperature;
        private static readonly AidlabAPI.callbackHeartRate PastHeartRateCallback = OnPastHeartRate;
        private static readonly AidlabAPI.callbackRr PastRrCallback = OnPastRr;
        private static readonly AidlabAPI.callbackActivity PastActivityCallback = OnPastActivity;
        private static readonly AidlabAPI.callbackRespirationRate PastRespirationRateCallback = OnPastRespirationRate;
        private static readonly AidlabAPI.callbackSteps PastStepsCallback = OnPastSteps;
        private static readonly AidlabAPI.callbackUserEvent PastUserEventCallback = OnPastUserEvent;
        private static readonly AidlabAPI.callbackSoundVolume PastSoundVolumeCallback = OnPastSoundVolume;
        private static readonly AidlabAPI.callbackPressure PastPressureCallback = OnPastPressure;
        private static readonly AidlabAPI.callbackAccelerometer PastAccelerometerCallback = OnPastAccelerometer;
        private static readonly AidlabAPI.callbackGyroscope PastGyroscopeCallback = OnPastGyroscope;
        private static readonly AidlabAPI.callbackQuaternion PastQuaternionCallback = OnPastQuaternion;
        private static readonly AidlabAPI.callbackOrientation PastOrientationCallback = OnPastOrientation;
        private static readonly AidlabAPI.callbackMagnetometer PastMagnetometerCallback = OnPastMagnetometer;
        private static readonly AidlabAPI.callbackBodyPosition PastBodyPositionCallback = OnPastBodyPosition;
        private static readonly AidlabAPI.callbackSignalQuality PastSignalQualityCallback = OnPastSignalQuality;
        private static readonly AidlabAPI.callbackEda PastEdaCallback = OnPastEda;
        private static readonly AidlabAPI.callbackGps PastGpsCallback = OnPastGps;

        private static readonly AidlabAPI.callbackBleSend BleSendCallback = OnBleSend;
        private static readonly AidlabAPI.callbackBleReady BleReadyCallback = OnBleReady;

        private static readonly AidlabAPI.callbackPayload PayloadCallback = OnPayload;
        private static readonly AidlabAPI.callbackProcessError ProcessErrorCallback = OnProcessError;
        private static readonly AidlabAPI.callbackError ErrorCallback = OnSdkError;

        private void Awake()
        {
            lastDataReceivedTime = -1.0f;
            receivedData = false;
            debugRxPackets = 0;
            debugRxBytes = 0;
            Interlocked.Exchange(ref debugSignalUpdates, 0);
            Interlocked.Exchange(ref debugHrCallbacks, 0);
            Interlocked.Exchange(ref debugRrCallbacks, 0);
            Interlocked.Exchange(ref debugWearStateCallbacks, 0);
            Interlocked.Exchange(ref debugLastHrAtMs, -1);
            Interlocked.Exchange(ref debugLastRrAtMs, -1);
            Interlocked.Exchange(ref debugLastWearStateAtMs, -1);
            Volatile.Write(ref debugLastRxSize, 0);
            debugCollectSentAtMs = -1;
            debugLastNoSignalLogAtMs = -1;
            lock (earlyRxLock)
            {
                earlyRxChunks.Clear();
                earlyRxBytesBuffered = 0;
            }
            bleConnector = null;
        }

        private void EnsureConnector()
        {
            if (bleConnector != null)
                return;

#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
            bleConnector = new BLEConnector(this, deviceNameToConnect);
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
            bleConnector = new MacBLEConnector(this, deviceNameToConnect);
#elif UNITY_IOS
            bleConnector = new IosBLEConnector(this, deviceNameToConnect);
#elif UNITY_ANDROID && !UNITY_EDITOR
            androidBackend = androidBackend ?? new AndroidAidlabBackend(this, deviceNameToConnect);
#else
            bleConnector = null;
#endif
        }

        private void Update()
        {
            EnsureConnector();
            UpdateLastPacketTime();
#if UNITY_ANDROID && !UNITY_EDITOR
            androidBackend?.Update();
#endif
            bleConnector?.ConnectionProcess();
            mainThreadWorker.Update();
            FlushPendingFrames();
            DebugMaybeLogNoSignals();
        }

	        private void OnDisable()
	        {
	            ShutdownTransport();
	        }

	        public void ShutdownTransport()
	        {
#if UNITY_ANDROID && !UNITY_EDITOR
                androidBackend?.Shutdown();
                androidBackend = null;
#endif
	            var connector = bleConnector;
	            bleConnector = null;
	            connector?.Disconnect();
	            DestroySdk();
	        }

        private void UpdateLastPacketTime()
        {
            if (receivedData)
            {
                lastDataReceivedTime = Time.time;
                receivedData = false;
            }
        }

        public void OnAidlabConnected()
        {
            if (sdk != IntPtr.Zero)
                return;

            debugRxPackets = 0;
            debugRxBytes = 0;
            Interlocked.Exchange(ref debugSignalUpdates, 0);
            Interlocked.Exchange(ref debugHrCallbacks, 0);
            Interlocked.Exchange(ref debugRrCallbacks, 0);
            Interlocked.Exchange(ref debugWearStateCallbacks, 0);
            Interlocked.Exchange(ref debugLastHrAtMs, -1);
            Interlocked.Exchange(ref debugLastRrAtMs, -1);
            Interlocked.Exchange(ref debugLastWearStateAtMs, -1);
            debugCollectSentAtMs = -1;
            debugLastNoSignalLogAtMs = -1;

            string normalizedFirmware = NormalizeFirmwareRevision(firmwareRevisionStr ?? "");
            if (!string.Equals(normalizedFirmware, firmwareRevisionStr ?? "", StringComparison.Ordinal))
            {
                if (enablePacketTrace)
                    Debug.Log($"AidlabSDK: normalized firmware '{firmwareRevisionStr}' -> '{normalizedFirmware}'");
                firmwareRevisionStr = normalizedFirmware;
            }

            byte[] firmware = Encoding.UTF8.GetBytes(firmwareRevisionStr ?? "");
            if (firmware.Length == 0)
            {
                Debug.LogError("AidlabSDK: firmwareRevisionStr is empty");
                return;
            }

            try
            {
                sdk = AidlabAPI.AidlabSDK_create(firmware, firmware.Length);
            }
            catch (Exception ex)
            {
                Debug.LogError($"AidlabSDK: AidlabSDK_create threw: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}");
                return;
            }
            if (sdk == IntPtr.Zero)
            {
                Debug.LogError("AidlabSDK: AidlabSDK_create returned null");
                return;
            }

            contextHandle = GCHandle.Alloc(this);
            IntPtr contextPtr = GCHandle.ToIntPtr(contextHandle);
            AidlabAPI.AidlabSDK_set_context(contextPtr, sdk);

            AidlabAPI.AidlabSDK_set_error_callback(ErrorCallback, contextPtr, sdk);
            AidlabAPI.AidlabSDK_set_payload_callback(PayloadCallback, sdk);
            AidlabAPI.AidlabSDK_set_process_error_callback(ProcessErrorCallback, sdk);

            AidlabAPI.AidlabSDK_set_ble_send_callback(BleSendCallback, sdk);
            AidlabAPI.AidlabSDK_set_ble_ready_callback(BleReadyCallback, sdk);

            FlushEarlyRxChunks();

            AidlabAPI.AidlabSDK_init_callbacks(
                EcgCallback,
                RespirationCallback,
                TemperatureCallback,
                AccelerometerCallback,
                GyroscopeCallback,
                MagnetometerCallback,
                BatteryCallback,
                ActivityCallback,
                StepsCallback,
                OrientationCallback,
                QuaternionCallback,
                RespirationRateCallback,
                WearStateCallback,
                HeartRateCallback,
                RrCallback,
                SoundVolumeCallback,
                ExerciseCallback,
                UserEventCallback,
                PressureCallback,
                PressureWearStateCallback,
                BodyPositionCallback,
                SignalQualityCallback,
                sdk);

            AidlabAPI.AidlabSDK_init_synchronization_callbacks(
                SyncStateCallback,
                UnsynchronizedSizeCallback,
                PastEcgCallback,
                PastRespirationCallback,
                PastTemperatureCallback,
                PastHeartRateCallback,
                PastRrCallback,
                PastActivityCallback,
                PastRespirationRateCallback,
                PastStepsCallback,
                PastUserEventCallback,
                PastSoundVolumeCallback,
                PastPressureCallback,
                PastAccelerometerCallback,
                PastGyroscopeCallback,
                PastQuaternionCallback,
                PastOrientationCallback,
                PastMagnetometerCallback,
                PastBodyPositionCallback,
                PastSignalQualityCallback,
                sdk);

            AidlabAPI.AidlabSDK_set_eda_callback(EdaCallback, sdk);
            AidlabAPI.AidlabSDK_set_gps_callback(GpsCallback, sdk);
            AidlabAPI.AidlabSDK_set_past_eda_callback(PastEdaCallback, sdk);
            AidlabAPI.AidlabSDK_set_past_gps_callback(PastGpsCallback, sdk);

            StartCollectAll();
            EnqueueMainThreadEvent(Connected);
        }

        private void FlushEarlyRxChunks()
        {
            Queue<byte[]> chunksToProcess = null;
            lock (earlyRxLock)
            {
                if (earlyRxChunks.Count == 0)
                    return;

                chunksToProcess = new Queue<byte[]>(earlyRxChunks);
                earlyRxChunks.Clear();
                earlyRxBytesBuffered = 0;
            }

            int count = chunksToProcess.Count;
            if (enablePacketTrace)
                Debug.Log($"AidlabSDK: flushing {count} early RX chunks");
            while (chunksToProcess.Count > 0)
            {
                byte[] chunk = chunksToProcess.Dequeue();
                if (chunk == null || chunk.Length == 0)
                    continue;
                AidlabAPI.AidlabSDK_process_ble_chunk(chunk, chunk.Length, sdk);
            }
        }

        private static string NormalizeFirmwareRevision(string value)
        {
            if (string.IsNullOrEmpty(value))
                return "";

            string trimmed = value.Trim();
            var match = Regex.Match(trimmed, @"(\d+)\.(\d+)\.(\d+)");
            if (match.Success)
            {
                return $"{match.Groups[1].Value}.{match.Groups[2].Value}.{match.Groups[3].Value}";
            }
            return trimmed;
        }

        public void StartCollectAll()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            _ = StartAndroidCollectAndReportFailure();
#else
            _ = StartCollectAllAsync();
#endif
        }

#if UNITY_ANDROID && !UNITY_EDITOR
        private async Task StartAndroidCollectAndReportFailure()
        {
            try
            {
                int? pid = await StartCollectAllAsync();
                if (!pid.HasValue)
                {
                    AndroidDidReceiveError(new AidlabError(
                        AidlabAPI.AidlabErrorCode.Protocol,
                        "Device rejected collect command"
                    ));
                }
            }
            catch (Exception exception)
            {
                AndroidDidReceiveError(new AidlabError(
                    AidlabAPI.AidlabErrorCode.Sdk,
                    $"Collect failed: {exception.Message}"
                ));
            }
        }
#endif

        public Task<int?> StartCollectAllAsync()
        {
            debugCollectSentAtMs = DebugNowMs();
#if UNITY_ANDROID && !UNITY_EDITOR
            if (androidBackend == null)
                return Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
            return androidBackend.Collect(liveSignalsToCollect, syncSignalsToStore);
#else
            string firmware = firmwareRevisionStr ?? "";
            int major = ParseMajorFirmware(firmware);

            if (major >= 4)
            {
                return StartCollectV4Flags();
            }

            if (CompareFirmware(firmware, "3.7.80") >= 0)
            {
                return StartCollectV3Flags();
            }

            if (CompareFirmware(firmware, "3.6.0") >= 0)
            {
                return StartCollectV3Binary();
            }

            legacyCollectionEnabled = true;
            SendCommand("collect");
            return Task.FromResult<int?>(null);
#endif
        }

	        private Task<int?> StartCollectV3Flags()
	        {
	            ushort liveFlags = (ushort)(BuildFlags16(liveSignalsToCollect) & 0xFFFF);
	            ushort syncFlags = (ushort)(BuildFlags16(syncSignalsToStore) & 0xFFFF);

            string liveHex = liveFlags.ToString("X4");
            string syncHex = syncFlags.ToString("X4");
            if (enablePacketTrace)
                Debug.Log($"AidlabSDK: collect flags {liveHex} {syncHex}");
            int? spawnedProcessId = HasCollectAutoSyncBug() ? (int?)SyncProcessId : null;
            return SendCommandAsync($"collect flags {liveHex} {syncHex}", spawnedProcessId);
        }

	        private Task<int?> StartCollectV3Binary()
	        {
	            uint liveFlags = BuildFlags16(liveSignalsToCollect);
	            uint syncFlags = BuildFlags16(syncSignalsToStore);

            byte[] prefix = Encoding.UTF8.GetBytes("collect on ");
            byte[] payload = new byte[prefix.Length + 8];
            Buffer.BlockCopy(prefix, 0, payload, 0, prefix.Length);

            int offset = prefix.Length;
            payload[offset++] = (byte)((liveFlags >> 24) & 0xFF);
            payload[offset++] = (byte)((liveFlags >> 16) & 0xFF);
            payload[offset++] = (byte)((liveFlags >> 8) & 0xFF);
            payload[offset++] = (byte)(liveFlags & 0xFF);
            payload[offset++] = (byte)((syncFlags >> 24) & 0xFF);
            payload[offset++] = (byte)((syncFlags >> 16) & 0xFF);
            payload[offset++] = (byte)((syncFlags >> 8) & 0xFF);
            payload[offset] = (byte)(syncFlags & 0xFF);

            if (enablePacketTrace)
                Debug.Log($"AidlabSDK: collect on (binary), liveFlags=0x{liveFlags:X8} syncFlags=0x{syncFlags:X8}");
            return SendProcessPayloadAsync(payload);
        }

		        private Task<int?> StartCollectV4Flags()
		        {
		            uint liveFlags = BuildFlags32(liveSignalsToCollect);
		            uint syncFlags = BuildFlags32(syncSignalsToStore);

            string liveHex = liveFlags.ToString("X8");
            string syncHex = syncFlags.ToString("X8");
            if (enablePacketTrace)
                Debug.Log($"AidlabSDK: collect flags {liveHex} {syncHex} (v4)");
            return SendCommandAsync($"collect flags {liveHex} {syncHex}");
        }

	        private static uint BuildFlags16(Signal[] signals)
	        {
	            // Firmware 3.x uses a 16-bit flags map (0..15).
	            if (signals == null || signals.Length == 0)
	                return 0;

	            uint flags = 0;
	            foreach (Signal signal in signals)
	            {
	                int bit = (int)signal;
	                if (bit < 0 || bit > 15)
	                    continue;
	                flags |= (1u << bit);
	            }
	            return flags;
	        }

	        private static uint BuildFlags32(Signal[] signals)
	        {
	            // Firmware 4.x uses a 32-bit flags map and supports newer signals (EDA/GPS).
	            if (signals == null || signals.Length == 0)
	                return 0;

	            uint flags = 0;
	            foreach (Signal signal in signals)
	            {
	                int bit = (int)signal;
	                if (bit < 0 || bit > 31)
	                    continue;
	                flags |= (1u << bit);
	            }
	            return flags;
	        }

        private static int CompareFirmware(string firmware, string reference)
        {
            int[] a = ParseVersionParts(firmware);
            int[] b = ParseVersionParts(reference);

            for (int i = 0; i < 3; i++)
            {
                if (a[i] != b[i])
                    return a[i].CompareTo(b[i]);
            }
            return 0;
        }

        private bool HasCollectAutoSyncBug()
        {
            return CompareFirmware(firmwareRevisionStr, "3.7.85") >= 0 &&
                   CompareFirmware(firmwareRevisionStr, "3.7.110") <= 0;
        }

        private static int[] ParseVersionParts(string value)
        {
            int[] parts = { 0, 0, 0 };
            if (string.IsNullOrEmpty(value))
                return parts;

            string[] raw = value.Trim().Split('.');
            for (int i = 0; i < 3 && i < raw.Length; i++)
            {
                int.TryParse(raw[i], out parts[i]);
            }
            return parts;
        }

        private static int ParseMajorFirmware(string firmware)
        {
            if (string.IsNullOrEmpty(firmware))
                return 0;

            int dot = firmware.IndexOf('.');
            string majorStr = dot >= 0 ? firmware.Substring(0, dot) : firmware;
            if (int.TryParse(majorStr, out int major))
                return major;
            return 0;
        }

        public void OnAidlabDataReceived(BLEApi.BLEData bleData)
        {
            receivedData = true;

            if (sdk == IntPtr.Zero)
                return;

            if (!AidlabCharacteristicsUUID.CmdUUID.Equals(bleData.serviceUuid, bleData.characteristicUuid))
                return;

            AidlabAPI.AidlabSDK_process_ble_chunk(bleData.buf, bleData.size, sdk);
        }

        public void OnAidlabDisconnected()
        {
            lastDataReceivedTime = -1.0f;
            receivedData = false;
            var connector = bleConnector;
            bleConnector = null;
            connector?.Disconnect();
            DestroySdk();
            firmwareRevisionStr = "";
            hardwareRevisionStr = "";
            EnqueueMainThreadEvent(Disconnected);
        }

	        private static void EnqueueMainThreadEvent(Action action)
	        {
	            if (action == null)
	                return;

	            var evt = new UnityEvent();
	            evt.AddListener(() => action());
	            MainThreadWorker.Enqueue(evt);
	        }

        public void SendCommand(string command)
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            if (androidBackend == null || string.IsNullOrEmpty(command))
                return;
            _ = androidBackend.SendCommand(command);
#else
            if (sdk == IntPtr.Zero)
                return;

            if (string.IsNullOrEmpty(command))
                return;

            byte[] payload = Encoding.UTF8.GetBytes(command + "\0");
            SendPayload(payload, 0);
#endif
        }

        public Task<int?> SendCommandAsync(string command)
        {
            return SendCommandAsync(command, null);
        }

        private Task<int?> SendCommandAsync(string command, int? spawnedProcessId)
        {
            if (string.IsNullOrEmpty(command))
                return Task.FromException<int?>(new ArgumentException("Command must not be empty", nameof(command)));

#if UNITY_ANDROID && !UNITY_EDITOR
            _ = spawnedProcessId;
            return androidBackend != null
                ? androidBackend.SendCommand(command)
                : Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
#else
            byte[] payload = Encoding.UTF8.GetBytes(command + "\0");
            return SendProcessPayloadAsync(payload, spawnedProcessId);
#endif
        }

        public Task<int?> StartSynchronization()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return androidBackend != null
                ? androidBackend.StartSynchronization()
                : Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
#else
            return SendCommandAsync(SynchronizationStartCommand());
#endif
        }

        private string SynchronizationStartCommand()
        {
            return CompareFirmware(firmwareRevisionStr ?? "", FastSyncMinimumFirmware) >= 0
                ? "sync fast"
                : "sync start";
        }

        public Task<int?> StopSynchronization()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return androidBackend != null
                ? androidBackend.StopSynchronization()
                : Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
#else
            int? activePid = null;
            lock (commandLock)
            {
                if (activeProcessPids.TryGetValue(SyncProcessId, out int pid))
                    activePid = pid;
            }
            if (activePid.HasValue)
                return SendActiveProcessCommandAsync("sync stop", activePid.Value);
            return Task.FromResult<int?>(null);
#endif
        }

        public Task<int?> ClearSynchronization()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return androidBackend != null
                ? androidBackend.ClearSynchronization()
                : Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
#else
            return SendCommandAsync("sync clear");
#endif
        }

        public Task<int?> StopCollect()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return androidBackend != null
                ? androidBackend.StopCollect()
                : Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
#else
            if (sdk == IntPtr.Zero)
                return Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
            if (CompareFirmware(firmwareRevisionStr ?? "", "3.6.0") < 0)
            {
                legacyCollectionEnabled = false;
                return Task.FromResult<int?>(null);
            }
            return SendCommandAsync("collect off");
#endif
        }

        private async Task<int?> SendProcessPayloadAsync(byte[] payload, int? spawnedProcessId = null)
        {
            await commandSemaphore.WaitAsync();
            try
            {
                return await SendProcessPayloadLockedAsync(payload, spawnedProcessId);
            }
            finally
            {
                commandSemaphore.Release();
            }
        }

        private async Task<int?> SendProcessPayloadLockedAsync(byte[] payload, int? spawnedProcessId)
        {
            if (sdk == IntPtr.Zero)
                throw new InvalidOperationException("SDK is not initialized");

            if (payload == null || payload.Length == 0)
                throw new ArgumentException("Payload must not be empty", nameof(payload));

            var completion = new PendingProcessCommand(spawnedProcessId);
            lock (commandLock)
            {
                if (pendingCommand != null)
                    throw new InvalidOperationException("Another process command is already pending");
                pendingCommand = completion;
            }

            try
            {
                if (!TrySendPayload(payload, 0, out Task frameConfirmation))
                    throw new InvalidOperationException("Previous BLE frame is not confirmed");

                Task.Delay(TimeSpan.FromSeconds(3)).ContinueWith(_ =>
                {
                    CompletePendingCommand(completion, new TimeoutException("Timed out waiting for process command result"));
                });
                await frameConfirmation;
                int? result = await completion.Response.Task;
                if (result.HasValue && completion.SpawnedProcess != null)
                {
                    int? spawnedResult = await completion.SpawnedProcess.Task;
                    if (!spawnedResult.HasValue)
                        throw new InvalidOperationException($"Firmware rejected required spawned process {spawnedProcessId}");
                }
                return result;
            }
            finally
            {
                lock (commandLock)
                {
                    if (ReferenceEquals(pendingCommand, completion))
                        pendingCommand = null;
                }
            }
        }

        private async Task<int?> SendActiveProcessCommandAsync(string command, int pid)
        {
            await commandSemaphore.WaitAsync();
            TaskCompletionSource<int?> completion = null;
            try
            {
                if (sdk == IntPtr.Zero)
                    throw new InvalidOperationException("SDK is not initialized");

                byte[] payload = Encoding.UTF8.GetBytes(command + "\0");
                completion = new TaskCompletionSource<int?>();
                lock (commandLock)
                {
                    if (pendingProcessTermination != null)
                        throw new InvalidOperationException("Another process termination is already pending");
                    pendingProcessTermination = completion;
                    pendingTerminationPid = pid;
                }

                if (!TryBeginFrameConfirmation(out Task frameConfirmation))
                    throw new InvalidOperationException("Previous BLE frame is not confirmed");
                if (!EmitTrackedFrame(() =>
                    AidlabAPI.AidlabSDK_send_process_command(payload, payload.Length, pid, sdk)))
                {
                    OnTransportWriteFailure("SDK rejected the BLE frame");
                }
                Task.Delay(TimeSpan.FromSeconds(3)).ContinueWith(_ =>
                {
                    CompletePendingProcessTermination(
                        completion,
                        new TimeoutException("Timed out waiting for process termination")
                    );
                });
                await frameConfirmation;
                int? result = await completion.Task;
                return result;
            }
            finally
            {
                lock (commandLock)
                {
                    if (completion != null && ReferenceEquals(pendingProcessTermination, completion))
                    {
                        pendingProcessTermination = null;
                        pendingTerminationPid = -1;
                    }
                }
                commandSemaphore.Release();
            }
        }

        private void CompletePendingProcessTermination(TaskCompletionSource<int?> expected, int? pid)
        {
            lock (commandLock)
            {
                if (!ReferenceEquals(pendingProcessTermination, expected))
                    return;
                pendingProcessTermination = null;
                pendingTerminationPid = -1;
            }
            expected.TrySetResult(pid);
        }

        private void CompletePendingProcessTermination(TaskCompletionSource<int?> expected, Exception exception)
        {
            lock (commandLock)
            {
                if (!ReferenceEquals(pendingProcessTermination, expected))
                    return;
                pendingProcessTermination = null;
                pendingTerminationPid = -1;
            }
            expected.TrySetException(exception);
        }

        private void CompletePendingCommand(PendingProcessCommand expected, Exception exception)
        {
            lock (commandLock)
            {
                if (!ReferenceEquals(pendingCommand, expected))
                    return;
                pendingCommand = null;
            }
            expected.Response.TrySetException(exception);
            expected.SpawnedProcess?.TrySetException(exception);
        }

        private void HandleProcessLifecycle(int status, int pid, int? processId)
        {
            TaskCompletionSource<int?> commandCompletion = null;
            TaskCompletionSource<int?> terminationCompletion = null;
            lock (commandLock)
            {
                if (processId.HasValue)
                {
                    if (status == SystemCreateSuccess)
                        activeProcessPids[processId.Value] = pid;
                    else if (status == SystemKillSuccess &&
                             activeProcessPids.TryGetValue(processId.Value, out int activePid) && activePid == pid)
                        activeProcessPids.Remove(processId.Value);
                }

                if (status == SystemCreateSuccess || status == SystemCreateFailure)
                {
                    if (pendingCommand != null && !pendingCommand.ResponseReceived)
                    {
                        pendingCommand.ResponseReceived = true;
                        commandCompletion = pendingCommand.Response;
                    }
                    else if (pendingCommand?.SpawnedProcess != null &&
                             (status == SystemCreateFailure || processId == pendingCommand.SpawnedProcessId))
                    {
                        commandCompletion = pendingCommand.SpawnedProcess;
                    }
                }
                else if (pendingProcessTermination != null && pendingTerminationPid == pid)
                {
                    terminationCompletion = pendingProcessTermination;
                    pendingProcessTermination = null;
                    pendingTerminationPid = -1;
                }
            }

            commandCompletion?.TrySetResult(status == SystemCreateSuccess ? (int?)pid : null);
            terminationCompletion?.TrySetResult(status == SystemKillSuccess ? (int?)pid : null);
            if (status == SystemKillSuccess)
                aidlabDelegate.ProcessDidTerminate(pid);
        }

        // Sends a raw payload to a runtime destination PID. Use processId 0 for shell/system commands.
        public void SendPayload(byte[] payload, int processId)
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            androidBackend?.Send(payload, processId);
#else
            if (sdk == IntPtr.Zero)
                return;

            if (payload == null || payload.Length == 0)
                return;

            if (!TrySendPayload(payload, processId, out _))
            {
                var error = new AidlabError(
                    AidlabAPI.AidlabErrorCode.Transport,
                    "Previous BLE frame is not confirmed"
                );
                aidlabDelegate.DidReceiveError(error);
                ErrorReceived?.Invoke(error);
                Debug.LogError(error.ToString());
            }
#endif
        }

        private bool TrySendPayload(byte[] payload, int processId, out Task frameConfirmation)
        {
            if (!TryBeginFrameConfirmation(out frameConfirmation))
                return false;

            if (!EmitTrackedFrame(() =>
                AidlabAPI.AidlabSDK_send(payload, payload.Length, processId, sdk)))
            {
                OnTransportWriteFailure("SDK rejected the BLE frame");
            }
            return true;
        }

        private void DestroySdk()
        {
            CompleteFrameConfirmation(new InvalidOperationException("SDK was destroyed"));
            PendingProcessCommand completion;
            TaskCompletionSource<int?> termination;
            lock (commandLock)
            {
                completion = pendingCommand;
                pendingCommand = null;
                termination = pendingProcessTermination;
                pendingProcessTermination = null;
                pendingTerminationPid = -1;
                activeProcessPids.Clear();
                legacyCollectionEnabled = false;
            }
            completion?.Response.TrySetException(new InvalidOperationException("SDK was destroyed"));
            completion?.SpawnedProcess?.TrySetException(new InvalidOperationException("SDK was destroyed"));
            termination?.TrySetException(new InvalidOperationException("SDK was destroyed"));

            if (sdk != IntPtr.Zero)
            {
                AidlabAPI.AidlabSDK_destroy(sdk);
                sdk = IntPtr.Zero;
            }

            if (contextHandle.IsAllocated)
                contextHandle.Free();

            lock (txLock)
            {
                pendingFrames.Clear();
            }
        }

        private void FlushPendingFrames()
        {
            byte[] frame = null;
            lock (txLock)
            {
                if (pendingFrames.Count > 0)
                    frame = pendingFrames.Dequeue();
            }

            if (frame == null)
                return;

            if (frame.Length > 0)
            {
                if (enablePacketTrace)
                {
                    string hex = BuildHexPreview(frame, frame.Length, 24);
                    Debug.Log($"AidlabSDK TX frame {frame.Length}B: {hex}");
                }
            }
            bleConnector?.Write(frame);
        }

        internal void OnTransportWriteFailure(string message)
        {
            EnqueueMainThreadEvent(() =>
            {
                CompleteFrameConfirmation(new InvalidOperationException(message));
                var error = new AidlabError(AidlabAPI.AidlabErrorCode.Transport, message);
                aidlabDelegate.DidReceiveError(error);
                ErrorReceived?.Invoke(error);
                Debug.LogError(message);
                OnAidlabDisconnected();
            });
        }

        internal void OnTransportFrameWritten(byte[] payload)
        {
            lock (frameConfirmationLock)
            {
                if (!ReferenceEquals(payload, trackedOutboundFrame))
                    return;
                trackedOutboundFrame = null;
            }

            if (!UsesV4Protocol())
            {
                CompleteFrameConfirmation();
                return;
            }

            Timer previousTimer;
            long generation;
            lock (frameConfirmationLock)
            {
                if (!awaitingFrameConfirmation)
                    return;

                generation = ++frameConfirmationGeneration;
                previousTimer = frameConfirmationTimer;
                frameConfirmationTimer = new Timer(
                    _ => EnqueueMainThreadEvent(() => HandleFrameConfirmationTimeout(generation)),
                    null,
                    FrameConfirmationTimeoutMs,
                    Timeout.Infinite
                );
            }
            previousTimer?.Dispose();
        }

        private bool UsesV4Protocol()
        {
            return ParseMajorFirmware(firmwareRevisionStr ?? "") >= 4;
        }

        private bool TryBeginFrameConfirmation(out Task confirmation)
        {
            Timer previousTimer = null;
            lock (frameConfirmationLock)
            {
                if (awaitingFrameConfirmation)
                {
                    confirmation = null;
                    return false;
                }

                awaitingFrameConfirmation = true;
                frameConfirmationCompletion = new TaskCompletionSource<bool>();
                confirmation = frameConfirmationCompletion.Task;
                ++frameConfirmationGeneration;
                previousTimer = frameConfirmationTimer;
                frameConfirmationTimer = null;
            }
            previousTimer?.Dispose();
            return true;
        }

        private bool EmitTrackedFrame(Action action)
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            lock (frameConfirmationLock)
            {
                expectedFrameCallbackThreadId = threadId;
            }

            action();

            lock (frameConfirmationLock)
            {
                bool emitted = expectedFrameCallbackThreadId != threadId;
                if (!emitted)
                    expectedFrameCallbackThreadId = -1;
                return emitted;
            }
        }

        private void CompleteFrameConfirmation(Exception error = null)
        {
            Timer deadline;
            TaskCompletionSource<bool> completion;
            lock (frameConfirmationLock)
            {
                awaitingFrameConfirmation = false;
                ++frameConfirmationGeneration;
                deadline = frameConfirmationTimer;
                completion = frameConfirmationCompletion;
                frameConfirmationTimer = null;
                frameConfirmationCompletion = null;
                expectedFrameCallbackThreadId = -1;
                trackedOutboundFrame = null;
            }
            deadline?.Dispose();
            if (error == null)
                completion?.TrySetResult(true);
            else
                completion?.TrySetException(error);
        }

        private void HandleFrameConfirmationTimeout(long generation)
        {
            Timer deadline;
            TaskCompletionSource<bool> completion;
            lock (frameConfirmationLock)
            {
                if (!awaitingFrameConfirmation || frameConfirmationGeneration != generation)
                    return;

                awaitingFrameConfirmation = false;
                ++frameConfirmationGeneration;
                deadline = frameConfirmationTimer;
                completion = frameConfirmationCompletion;
                frameConfirmationTimer = null;
                frameConfirmationCompletion = null;
                expectedFrameCallbackThreadId = -1;
                trackedOutboundFrame = null;
            }
            deadline?.Dispose();

            var timeout = new TimeoutException("BLE frame confirmation timed out");
            completion?.TrySetException(timeout);
            var error = new AidlabError(
                AidlabAPI.AidlabErrorCode.Transport,
                "BLE frame confirmation timed out"
            );
            aidlabDelegate.DidReceiveError(error);
            ErrorReceived?.Invoke(error);
            Debug.LogError(error.ToString());
            OnAidlabDisconnected();
        }

        public void OnAidlabDataReceived(byte[] data, int size)
        {
            receivedData = true;

            if (data == null || size <= 0)
                return;

            long packetIndex = Interlocked.Increment(ref debugRxPackets);
            Interlocked.Add(ref debugRxBytes, size);
            Interlocked.Exchange(ref debugLastRxAtMs, DebugNowMs());
            Volatile.Write(ref debugLastRxSize, size);

            if (enablePacketTrace && (packetIndex <= 5 || (packetIndex % 200) == 0))
            {
                string hex = BuildHexPreview(data, size, 32);
                if (packetIndex <= 5)
                    Debug.Log($"AidlabSDK RX[{packetIndex}] {size}B: {hex}");
            }

            if (sdk == IntPtr.Zero)
            {
                lock (earlyRxLock)
                {
                    if (earlyRxBytesBuffered + size <= EarlyRxMaxBytes)
                    {
                        var copy = new byte[size];
                        Buffer.BlockCopy(data, 0, copy, 0, size);
                        earlyRxChunks.Enqueue(copy);
                        earlyRxBytesBuffered += size;
                    }
                }
                return;
            }

            AidlabAPI.AidlabSDK_process_ble_chunk(data, size, sdk);
        }

        private static string BuildHexPreview(byte[] data, int size, int maxBytes)
        {
            if (data == null || size <= 0)
                return "";

            int len = Math.Min(size, Math.Max(0, maxBytes));
            var sb = new StringBuilder(len * 2 + 4);
            for (int i = 0; i < len; i++)
            {
                sb.Append(data[i].ToString("X2"));
            }
            if (len < size)
                sb.Append("…");
            return sb.ToString();
        }

        private void DebugMaybeLogNoSignals()
        {
            if (sdk == IntPtr.Zero)
                return;

            long collectAt = Interlocked.Read(ref debugCollectSentAtMs);
            if (collectAt <= 0)
                return;

            long now = DebugNowMs();
            if (now - collectAt < 2500)
                return;

            long rxPacketsSnapshot = Interlocked.Read(ref debugRxPackets);
            if (rxPacketsSnapshot <= 0)
                return;

            long updatesSnapshot = Interlocked.Read(ref debugSignalUpdates);
            if (updatesSnapshot > 0)
                return;

            long lastLogAt = Interlocked.Read(ref debugLastNoSignalLogAtMs);
            if (lastLogAt > 0 && now - lastLogAt < 5000)
                return;

            int lastSize = Volatile.Read(ref debugLastRxSize);
            long lastRxAt = Interlocked.Read(ref debugLastRxAtMs);
            long ageMs = lastRxAt > 0 ? (now - lastRxAt) : -1;

            Debug.LogWarning(
                $"AidlabSDK: RX={rxPacketsSnapshot} packets ({Interlocked.Read(ref debugRxBytes)}B) but no signal callbacks after collect. " +
                $"LastRX={lastSize}B ageMs={ageMs}"
            );
            Interlocked.Exchange(ref debugLastNoSignalLogAtMs, now);
        }

        internal static void DebugNotifySignalUpdated()
        {
            Interlocked.Increment(ref debugSignalUpdates);
        }

#if UNITY_ANDROID && !UNITY_EDITOR
        internal void AndroidDidConnect(string firmwareRevision, string hardwareRevision)
        {
            firmwareRevisionStr = NormalizeFirmwareRevision(firmwareRevision ?? "");
            hardwareRevisionStr = hardwareRevision ?? "";
            lastDataReceivedTime = -1.0f;
            receivedData = false;
            debugRxPackets = 0;
            debugRxBytes = 0;
            Interlocked.Exchange(ref debugSignalUpdates, 0);
            StartCollectAll();
            EnqueueMainThreadEvent(Connected);
        }

        internal void AndroidDidDisconnect()
        {
            firmwareRevisionStr = "";
            hardwareRevisionStr = "";
            lastDataReceivedTime = -1.0f;
            receivedData = false;
            EnqueueMainThreadEvent(Disconnected);
        }

        internal void AndroidDidReceiveData()
        {
            receivedData = true;
            Interlocked.Increment(ref debugRxPackets);
            Interlocked.Exchange(ref debugLastRxAtMs, DebugNowMs());
        }

        internal void AndroidDidReceiveError(AidlabError error)
        {
            if (error == null)
                return;
            aidlabDelegate.DidReceiveError(error);
            EnqueueMainThreadEvent(() => ErrorReceived?.Invoke(error));
            Debug.LogError(error.ToString());
        }

        internal void AndroidDidReceivePayload(string process, byte[] payload, ulong options)
        {
            byte[] data = payload ?? Array.Empty<byte>();
            EnqueueMainThreadEvent(() => PayloadReceived?.Invoke(process ?? "", data, options));
        }

        internal void AndroidDidReceiveProcessError(string process, int pid, byte[] payload, ulong options)
        {
            byte[] data = payload ?? Array.Empty<byte>();
            EnqueueMainThreadEvent(() => ProcessErrorReceived?.Invoke(process ?? "", pid, data, options));
        }

        internal void AndroidDidTerminateProcess(int pid)
        {
            aidlabDelegate.ProcessDidTerminate(pid);
        }
#endif

        private static AidlabSDK GetInstance(IntPtr context)
        {
            if (context == IntPtr.Zero)
                return null;

            GCHandle handle = GCHandle.FromIntPtr(context);
            return handle.Target as AidlabSDK;
        }

        private static void OnBleSend(IntPtr context, IntPtr data, int size)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance == null)
                return;

            if (data == IntPtr.Zero || size <= 0)
                return;

            byte[] frame = new byte[size];
            Marshal.Copy(data, frame, 0, size);

            lock (instance.frameConfirmationLock)
            {
                if (instance.expectedFrameCallbackThreadId == Thread.CurrentThread.ManagedThreadId)
                {
                    instance.expectedFrameCallbackThreadId = -1;
                    instance.trackedOutboundFrame = frame;
                }
            }

            if (instance.enablePacketTrace)
            {
                string hex = BuildHexPreview(frame, size, 32);
                Debug.Log($"AidlabSDK: OnBleSend {size}B: {hex}");
            }

            lock (instance.txLock)
            {
                instance.pendingFrames.Enqueue(frame);
            }
        }

        private static void OnBleReady(IntPtr context)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance == null)
                return;

            instance.CompleteFrameConfirmation();
            if (instance.enablePacketTrace)
                Debug.Log("AidlabSDK: BLE ready");
        }

        private static void OnPayload(IntPtr context, IntPtr process, IntPtr payload, UIntPtr payloadLength, ulong options)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance == null)
                return;

            string processName = process == IntPtr.Zero ? "" : Marshal.PtrToStringAnsi(process);
            int length = checked((int)payloadLength.ToUInt64());
            byte[] bytes = new byte[length];
            if (payload != IntPtr.Zero && length > 0)
                Marshal.Copy(payload, bytes, 0, length);

            if (string.Equals(processName, "system", StringComparison.OrdinalIgnoreCase) && bytes.Length > 0)
            {
                int status = bytes[0];
                if (status >= SystemCreateSuccess && status <= SystemKillFailure)
                {
                    int pid = bytes.Length >= 3 ? bytes[1] | (bytes[2] << 8) : 0;
                    int? processId = bytes.Length >= 4 ? bytes[3] : (int?)null;
                    instance.HandleProcessLifecycle(status, pid, processId);
                }
            }

            EnqueueMainThreadEvent(() => instance.PayloadReceived?.Invoke(processName, bytes, options));
        }

        private static void OnProcessError(IntPtr context, IntPtr process, ushort pid, IntPtr payload, UIntPtr payloadLength, ulong options)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance == null)
                return;

            string processName = process == IntPtr.Zero ? "" : Marshal.PtrToStringAnsi(process);
            int length = checked((int)payloadLength.ToUInt64());
            byte[] bytes = new byte[length];
            if (payload != IntPtr.Zero && length > 0)
                Marshal.Copy(payload, bytes, 0, length);
            instance.ProcessErrorReceived?.Invoke(processName, pid, bytes, options);
        }

        private static void OnSdkError(IntPtr context, AidlabAPI.AidlabErrorCode code, IntPtr message)
        {
            if (message == IntPtr.Zero)
                return;

            string text = Marshal.PtrToStringAnsi(message);
            var error = new AidlabError(code, text);
            AidlabSDK instance = GetInstance(context);
            instance?.CompleteFrameConfirmation(new InvalidOperationException(text));
            aidlabDelegate.DidReceiveError(error);
            instance?.ErrorReceived?.Invoke(error);
            Debug.LogError(error.ToString());
        }

        private static bool ShouldForwardLiveData(IntPtr context)
        {
            AidlabSDK instance = GetInstance(context);
            return instance == null ||
                   CompareFirmware(instance.firmwareRevisionStr ?? "", "3.6.0") >= 0 ||
                   instance.legacyCollectionEnabled;
        }

        private static void OnEcg(IntPtr context, ulong timestamp, float value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveECG(timestamp, value); }
        private static void OnRespiration(IntPtr context, ulong timestamp, float value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveRespiration(timestamp, value); }
        private static void OnTemperature(IntPtr context, ulong timestamp, float value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveSkinTemperature(timestamp, value); }
        private static void OnAccelerometer(IntPtr context, ulong timestamp, float ax, float ay, float az) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveAccelerometer(timestamp, ax, ay, az); }
        private static void OnGyroscope(IntPtr context, ulong timestamp, float gx, float gy, float gz) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveGyroscope(timestamp, gx, gy, gz); }
        private static void OnMagnetometer(IntPtr context, ulong timestamp, float mx, float my, float mz) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveMagnetometer(timestamp, mx, my, mz); }
        private static void OnBattery(IntPtr context, byte stateOfCharge) { _ = context; aidlabDelegate.DidReceiveBatteryLevel(stateOfCharge); }
        private static void OnActivity(IntPtr context, ulong timestamp, ActivityType activity) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveActivity(timestamp, activity); }
        private static void OnSteps(IntPtr context, ulong timestamp, ulong steps) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveSteps(timestamp, steps); }
        private static void OnOrientation(IntPtr context, ulong timestamp, float roll, float pitch, float yaw) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveOrientation(timestamp, roll, pitch, yaw); }
        private static void OnQuaternion(IntPtr context, ulong timestamp, float qw, float qx, float qy, float qz) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveQuaternion(timestamp, qw, qx, qy, qz); }
        private static void OnRespirationRate(IntPtr context, ulong timestamp, uint value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveRespirationRate(timestamp, value); }
        private static void OnWearState(IntPtr context, WearState wearState)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance != null)
            {
                Interlocked.Increment(ref instance.debugWearStateCallbacks);
                Interlocked.Exchange(ref instance.debugLastWearStateAtMs, DebugNowMs());
            }
            aidlabDelegate.WearStateDidChange(wearState);
        }

        private static void OnHeartRate(IntPtr context, ulong timestamp, int heartRate)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance != null)
            {
                Interlocked.Increment(ref instance.debugHrCallbacks);
                Interlocked.Exchange(ref instance.debugLastHrAtMs, DebugNowMs());
            }
            if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveHeartRate(timestamp, heartRate);
        }

        private static void OnRr(IntPtr context, ulong timestamp, int rr)
        {
            AidlabSDK instance = GetInstance(context);
            if (instance != null)
            {
                Interlocked.Increment(ref instance.debugRrCallbacks);
                Interlocked.Exchange(ref instance.debugLastRrAtMs, DebugNowMs());
            }
            if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveRr(timestamp, rr);
        }
        private static void OnSoundVolume(IntPtr context, ulong timestamp, ushort value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveSoundVolume(timestamp, value); }
        private static void OnExercise(IntPtr context, Exercise exercise) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidDetectExercise(exercise); }
        private static void OnUserEvent(IntPtr context, ulong timestamp) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidDetectUserEvent(timestamp); }
        private static void OnPressure(IntPtr context, ulong timestamp, int value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceivePressure(timestamp, value); }
        private static void OnPressureWearState(IntPtr context, WearState wearState) { _ = context; aidlabDelegate.PressureWearStateDidChange(wearState); }
        private static void OnBodyPosition(IntPtr context, ulong timestamp, BodyPosition value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveBodyPosition(timestamp, value); }
        private static void OnSignalQuality(IntPtr context, ulong timestamp, byte value) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveSignalQuality(timestamp, value); }
        private static void OnEda(IntPtr context, ulong timestamp, float conductance) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveEda(timestamp, conductance); }
        private static void OnGps(IntPtr context, ulong timestamp, float lat, float lon, float alt, float speed, float heading, float hdop) { if (ShouldForwardLiveData(context)) aidlabDelegate.DidReceiveGps(timestamp, lat, lon, alt, speed, heading, hdop); }

        private static void OnSyncState(IntPtr context, SyncState syncState) { _ = context; aidlabSyncDelegate.SyncStateDidChange(syncState); }
        private static void OnUnsynchronizedSize(IntPtr context, uint size, float progress) { _ = context; aidlabSyncDelegate.DidReceiveUnsynchronizedSize(size, progress); }

        private static void OnPastEcg(IntPtr context, ulong timestamp, float value) { _ = context; aidlabSyncDelegate.DidReceivePastEcg(timestamp, value); }
        private static void OnPastRespiration(IntPtr context, ulong timestamp, float value) { _ = context; aidlabSyncDelegate.DidReceivePastRespiration(timestamp, value); }
        private static void OnPastTemperature(IntPtr context, ulong timestamp, float value) { _ = context; aidlabSyncDelegate.DidReceivePastTemperature(timestamp, value); }
        private static void OnPastHeartRate(IntPtr context, ulong timestamp, int value) { _ = context; aidlabSyncDelegate.DidReceivePastHeartRate(timestamp, value); }
        private static void OnPastRr(IntPtr context, ulong timestamp, int value) { _ = context; aidlabSyncDelegate.DidReceivePastRr(timestamp, value); }
        private static void OnPastActivity(IntPtr context, ulong timestamp, ActivityType value) { _ = context; aidlabSyncDelegate.DidReceivePastActivity(timestamp, value); }
        private static void OnPastRespirationRate(IntPtr context, ulong timestamp, uint value) { _ = context; aidlabSyncDelegate.DidReceivePastRespirationRate(timestamp, value); }
        private static void OnPastSteps(IntPtr context, ulong timestamp, ulong value) { _ = context; aidlabSyncDelegate.DidReceivePastSteps(timestamp, value); }
        private static void OnPastUserEvent(IntPtr context, ulong timestamp) { _ = context; aidlabSyncDelegate.DidReceivePastUserEvent(timestamp); }
        private static void OnPastSoundVolume(IntPtr context, ulong timestamp, ushort value) { _ = context; aidlabSyncDelegate.DidReceivePastSoundVolume(timestamp, value); }
        private static void OnPastPressure(IntPtr context, ulong timestamp, int value) { _ = context; aidlabSyncDelegate.DidReceivePastPressure(timestamp, value); }
        private static void OnPastAccelerometer(IntPtr context, ulong timestamp, float ax, float ay, float az) { _ = context; aidlabSyncDelegate.DidReceivePastAccelerometer(timestamp, ax, ay, az); }
        private static void OnPastGyroscope(IntPtr context, ulong timestamp, float gx, float gy, float gz) { _ = context; aidlabSyncDelegate.DidReceivePastGyroscope(timestamp, gx, gy, gz); }
        private static void OnPastQuaternion(IntPtr context, ulong timestamp, float qw, float qx, float qy, float qz) { _ = context; aidlabSyncDelegate.DidReceivePastQuaternion(timestamp, qw, qx, qy, qz); }
        private static void OnPastOrientation(IntPtr context, ulong timestamp, float roll, float pitch, float yaw) { _ = context; aidlabSyncDelegate.DidReceivePastOrientation(timestamp, roll, pitch, yaw); }
        private static void OnPastMagnetometer(IntPtr context, ulong timestamp, float mx, float my, float mz) { _ = context; aidlabSyncDelegate.DidReceivePastMagnetometer(timestamp, mx, my, mz); }
        private static void OnPastBodyPosition(IntPtr context, ulong timestamp, BodyPosition value) { _ = context; aidlabSyncDelegate.DidReceivePastBodyPosition(timestamp, value); }
        private static void OnPastSignalQuality(IntPtr context, ulong timestamp, byte value) { _ = context; aidlabSyncDelegate.DidReceivePastSignalQuality(timestamp, value); }
        private static void OnPastEda(IntPtr context, ulong timestamp, float conductance) { _ = context; aidlabSyncDelegate.DidReceivePastEda(timestamp, conductance); }
        private static void OnPastGps(IntPtr context, ulong timestamp, float lat, float lon, float alt, float speed, float heading, float hdop) { _ = context; aidlabSyncDelegate.DidReceivePastGps(timestamp, lat, lon, alt, speed, heading, hdop); }
    }
}
