using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.Exceptions;
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Services.BluetoothLock.Crypto;
using TINK.Services.BluetoothLock.Tdo;
using System.Threading;
using System.Collections.Generic;
using TINK.Services.BluetoothLock.Exception;
using Xamarin.Essentials;
using TINK.Model.Connector;
using TINK.Model.Device;
using Polly.Retry;
using Polly;

namespace TINK.Services.BluetoothLock.BLE
{
    /// <summary> Manages a single lock.</summary>
    public abstract class LockItBase : ILockService
    {
        /// <summary> Lenght of seed in bytes.</summary>
        private const int SEEDLENGTH = 16;


        /// <summary> Timeout for open/ close operations.</summary>
        protected const int OPEN_CLOSE_TIMEOUT_MS = 30000;

        /// <summary> Timeout for get service operations.</summary>
        private const int GETSERVICE_TIMEOUT_MS = 3000;

        /// <summary> Timeout for read operations.</summary>
        private const int READ_TIMEOUT_MS = 3000;

        protected LockItBase(IDevice device, IAdapter adapter, ICipher cipher)
        {
            Device = device ?? throw new ArgumentException(nameof(device));
            Cipher = cipher ?? throw new ArgumentException(nameof(cipher));
            Adapter = adapter ?? throw new ArgumentException(nameof(adapter));

            _retryPollicy = Policy<byte[]>
                .Handle<CharacteristicReadException>()
                .WaitAndRetryAsync(2, index => TimeSpan.FromMilliseconds(100));

            GetGuid();
            GetName();

            if (InvalidatedSeed.ContainsKey(Guid))
            {
                // Lock was already connected. No need to add entry.
                return;
            }

            InvalidatedSeed.Add(Guid, new List<string>());
        }

        private static readonly Dictionary<Guid, List<string>> InvalidatedSeed = new Dictionary<Guid, List<string>>();

        /// <summary> Count of write- actions to activate lock characteristic..</summary>
        protected int ActivateLockWriteCounter { get; private set; }

        protected ICipher Cipher { get; }

        protected IAdapter Adapter { get; }

        protected IDevice Device { get; }

        private IService LockControl { get; set; }

        private IService BatteryControl { get; set; }

        private ICharacteristic ActivateLockCharacteristic { get; set; }

        private ICharacteristic AlarmCharacteristic { get; set; }

        private ICharacteristic AuthCharacteristic { get; set; }

        private ICharacteristic StateCharacteristic { get; set; }

        private ICharacteristic SoundCharacteristic { get; set; }

        private ICharacteristic BatteryCharacteristic { get; set; }

        private readonly AsyncRetryPolicy<byte[]> _retryPollicy;

        /// <summary> Gets the lock control service.</summary>
        private async Task<IService> GetLockControlService()
        {
            if (LockControl != null) return LockControl;

            LockControl = null;

            Log.ForContext<LockItBase>().Debug("Request to get lock control service.");

            var cts = new CancellationTokenSource();
            cts.CancelAfter(GETSERVICE_TIMEOUT_MS);
            try
            {
                LockControl = await Device.GetServiceAsync(new Guid("0000f00d-1212-efde-1523-785fef13d123"), cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting lock control service failed. {Exception}", exception);
                throw new System.Exception($"Can not get lock control service. {exception.Message}", exception);
            }
            finally
            {
                cts.Dispose();
            }

            Log.ForContext<LockItBase>().Debug("Getting lock control service succeeded.");
            return LockControl;
        }

        /// <summary> Gets battery service.</summary>
        private async Task<IService> GetBatteryService()
        {
            if (BatteryControl != null) return BatteryControl;

            BatteryControl = null;

            Log.ForContext<LockItBase>().Debug("Request to get battery control service.");

            var cts = new CancellationTokenSource();
            cts.CancelAfter(GETSERVICE_TIMEOUT_MS);
            try
            {
                BatteryControl = await Device.GetServiceAsync(new Guid("0000180F-0000-1000-8000-00805f9b34fb"), cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting battery service failed. {Exception}", exception);
                throw new System.Exception($"Can not get battery service. {exception.Message}", exception);
            }
            finally
            {
                cts.Dispose();
            }

            Log.ForContext<LockItBase>().Debug("Getting battery service succeeded.");
            return BatteryControl;
        }

        /// <summary> Gets the state characteristic.</summary>
        private async Task<ICharacteristic> GetActivateLockCharacteristicAsync()
        {
            if (ActivateLockCharacteristic != null) return ActivateLockCharacteristic;

            ActivateLockCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get activate lock characteristic.");

            try
            {
                ActivateLockCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting activate lock charcteristic failed. {Exception}", exception);
                throw new System.Exception($"Can not get activate characteristic. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug("Activate lock characteristic retrieved successfully.");

            return ActivateLockCharacteristic;
        }
        /// <summary> Gets the alarm characteristic.</summary>
        private async Task<ICharacteristic> GetAlarmCharacteristicAsync()
        {
            if (AlarmCharacteristic != null) return AlarmCharacteristic;

            AlarmCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get alarm characteristic.");

            try
            {
                AlarmCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BFFF-1212-efde-1523-785fef13d123"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting alarm-charcteristic failed. {Exception}", exception);
                throw new System.Exception($"Can not get alarm characteristic. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug("Get alarm characteristic retrieved successfully.");
            return AlarmCharacteristic;
        }

        /// <summary> Gets the auth characteristic.</summary>
        private async Task<ICharacteristic> GetAuthCharacteristicAsync()
        {
            if (AuthCharacteristic != null) return AuthCharacteristic;

            AuthCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get auth characteristic.");
            try
            {
                AuthCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting auth-charcteristic failed. {Exception}", exception);
                throw new System.Exception(string.Format("Can not get auth characteristic. {0}", exception.Message), exception);
            }

            Log.ForContext<LockItBase>().Debug("Get auth characteristic retrieved successfully.");
            return AuthCharacteristic;
        }

        /// <summary> Gets the state characteristic.</summary>
        protected async Task<ICharacteristic> GetStateCharacteristicAsync()
        {
            if (StateCharacteristic != null) return StateCharacteristic;

            StateCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get lock state characteristic.");
            try
            {
                StateCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting state charcteristic failed. {Exception}", exception);
                throw new System.Exception(string.Format("Can not get state characteristic. {0}", exception.Message), exception);
            }

            Log.ForContext<LockItBase>().Debug("Get state characteristic retrieved successfully.");
            return StateCharacteristic;
        }

        /// <summary> Gets the sound characteristic.</summary>
        private async Task<ICharacteristic> GetSoundCharacteristicAsync()
        {
            if (SoundCharacteristic != null) return SoundCharacteristic;

            SoundCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get sound characteristic.");
            try
            {
                SoundCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BAAE-1212-efde-1523-785fef13d123"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting sound charcteristic failed. {Exception}", exception);
                throw new System.Exception($"Can not get sound characteristic. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug("Get sound characteristic retrieved successfully.");
            return SoundCharacteristic;
        }

        /// <summary> Gets the battery characteristic.</summary>
        protected async Task<ICharacteristic> GetBatteryCharacteristicAsync()
        {
            if (BatteryCharacteristic != null) return BatteryCharacteristic;

            BatteryCharacteristic = null;

            Log.ForContext<LockItBase>().Debug("Request to get battery characteristic.");
            try
            {
                BatteryCharacteristic = await (await GetBatteryService())?.GetCharacteristicAsync(new Guid("00002a19-0000-1000-8000-00805f9b34fb"));
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Getting battery charcteristic failed. {Exception}", exception);
                throw new System.Exception($"Can not get battery characteristic. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug("Get battery characteristic retrieved successfully.");
            return BatteryCharacteristic;
        }

        /// <summary> Query name of lock.</summary>
        private void GetName()
        {
            if (!string.IsNullOrEmpty(Name))
            {
                // Prevent valid name to be queried more than twice because Name does not change.
                return;
            }

            Log.ForContext<LockItBase>().Debug("Query name of lock.");
            try
            {
                Name = Device.Name ?? string.Empty;
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving bluetooth name failed . {Exception}", exception);
                throw new System.Exception($"Can not get name of lock. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug($"Lock name is {Name}.");
            Id = Name.GetBluetoothLockId();
            return;
        }

        /// <summary> Full idvertisement name.</summary>
        public string Name { get; private set; } = string.Empty;

        /// <summary> Id part of idvertisement name.</summary>
        public int Id { get; private set; }

        /// <summary> Query GUID of lock.</summary>
        private void GetGuid()
        {
            if (Guid != TextToLockItTypeHelper.INVALIDLOCKGUID)
            {
                // Prevent valid GUID to be queried more than twice because GUID does not change.
            }

            Log.ForContext<LockItBase>().Debug("Query name of lock.");
            try
            {
                Guid = Device.Id;
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving bluetooth guid failed. {Exception}", exception);
                throw new System.Exception($"Can not get guid of lock. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug($"Lock GUID is {Guid}.");
        }

        /// <summary> GUID.</summary>
        public Guid Guid { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKGUID;

        private byte[] CopriKey { get; set; } = new byte[0];

        /// <summary> Gets the device state.</summary>
        public DeviceState? GetDeviceState()
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not get device state. Bluetooth code must be run on main thread");
            }

            DeviceState? state;
            Log.ForContext<LockItBase>().Debug("Request to get connection state.");
            try
            {
                state = GetDeviceState(Device.State);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving bluetooth state failed. {Exception}", exception);
                throw new System.Exception($"Can not get bluetooth state. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug($"Connection state is {state}.");
            return state;
        }

        /// <summary> Reconnects to device. </summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="authInfo">Info required to connect.</param>
        /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
        /// <returns>True if connecting succeeded, false if not.</returns>
        public abstract Task ReconnectAsync(
            LockInfoAuthTdo authInfo,
            TimeSpan connectTimeout);

        /// <summary> Reconnects to device. </summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="authInfo">Info required to connect.</param>
        /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
        /// <returns>True if connecting succeeded, false if not.</returns>
        protected async Task ReconnectAsync(
        LockInfoAuthTdo authInfo,
        TimeSpan connectTimeout,
        Func<LockItBase> factory)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not reconnect to lock. Bluetooth code must be run on main thread");
            }

            // Check if key is available.
            if (authInfo == null)
            {
                Log.ForContext<LockItBase>().Error($"Can not authenticate. No auth info available.");
                throw new AuthKeyException($"Can not authenticate. No auth info available.");
            }

            if (authInfo.K_seed.Length != SEEDLENGTH
                || authInfo.K_u.Length <= 0)
            {
                throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-lenght {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected.");
            }

            if (Device.State == Plugin.BLE.Abstractions.DeviceState.Connected)
            {
                throw new AlreadyConnectedException();
            }

            // Reset all references to characteristics.
            LockControl = null;
            ActivateLockCharacteristic = null;
            AlarmCharacteristic = null;
            AuthCharacteristic = null;
            StateCharacteristic = null;
            SoundCharacteristic = null;
            ActivateLockWriteCounter = 0;

            var cts = new CancellationTokenSource(connectTimeout);

            // Connect to device and authenticate.
            Log.ForContext<LockItBase>().Debug($"Request connect to device {Device.Name}. Connect state is {Device.State}.");
            try
            {
                await Adapter.ConnectToDeviceAsync(
                    Device,
                    new ConnectParameters(forceBleTransport: true /* Force BLE transport */),
                    cts.Token);
            }
            catch (System.Exception exception)
            {
                if (exception is TaskCanceledException)
                {
                    // A timeout occurred.
                    throw new System.Exception($"Can not reconnect.\r\nTimeout of {connectTimeout.TotalMilliseconds} [ms] elapsed.", exception);
                }

                Log.ForContext<LockItBase>().Error("Can not reconnect. {Exception}", exception);
                throw new System.Exception($"Can not Reconnect. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug($"Connecting to device succeeded. Starting auth sequence...");

            var lockIt = await Authenticate(Device, authInfo, Adapter, Cipher, factory);
            CopriKey = lockIt.CopriKey;
        }

        /// <summary> Connects to device. </summary>
        /// <param name="authInfo">Info required to connect.</param>
        /// <param name="device">Device with must be connected.</param>
        /// <returns>True if connecting succeeded, false if not.</returns>
        protected static async Task<LockItBase> Authenticate(
            IDevice device,
            LockInfoAuthTdo authInfo,
            IAdapter adapter,
            ICipher cipher,
            Func<LockItBase> factory)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not authenticate. Bluetooth code must be run on main thread");
            }

            if (device == null)
                throw new ArgumentException(nameof(device));

            if (cipher == null)
                throw new ArgumentException(nameof(cipher));

            if (adapter == null)
                throw new ArgumentException(nameof(adapter));

            // Check if connect is required.
            DeviceState deviceState;
            Log.ForContext<LockItBase>().Debug("Retrieving connection state is in context of auth.");
            try
            {
                deviceState = GetDeviceState(device.State);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Can not authenticate. Retrieving bluetooth state failed. {Exception}", exception);
                throw new System.Exception(string.Format("Can not authenticate. Getting bluetooth state failed. {0}", exception.Message), exception);
            }

            switch (deviceState)
            {
                case DeviceState.Disconnected:
                    throw new BluetoothDisconnectedException();

                case DeviceState.Connected:
                    break;

                default:
                    // Can not get state if device is not connected.
                    Log.ForContext<LockItBase>().Error($"Can not authenticate. Unexpected lock state {deviceState} detected.");
                    throw new System.Exception(string.Format("Can not authenticate. Unexpected bluetooth state {0} detected.", deviceState));
            }

            // Check if key is available.
            if (authInfo == null)
            {
                Log.ForContext<LockItBase>().Error($"Can not authenticate. No auth info available.");
                throw new AuthKeyException($"Can not authenticate. No auth info available.");
            }

            if (authInfo.K_seed.Length != SEEDLENGTH
                || authInfo.K_u.Length <= 0)
            {
                throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-lenght {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected.");
            }

            Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState} in context of auth.");

            var lockIt = factory();

            if (InvalidatedSeed[lockIt.Guid].Contains(string.Join(",", authInfo.K_seed)))
            {
                throw new AuthKeyException($"Can not authenticate. Seed {string.Join(",", authInfo.K_seed)} was already used.");
            }

            InvalidatedSeed[lockIt.Guid].Add(string.Join(",", authInfo.K_seed));

            // Connect to device and authenticate.
            try
            {
                await AuthenticateAsync(
                    lockIt,
                    authInfo,
                   lockIt.Cipher);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Authentication failed. {Exception}", exception);
                try
                {
                    // Disconnect from device if auth did not succeed.
                    await lockIt.Adapter.DisconnectDeviceAsync(lockIt.Device);
                }
                catch (System.Exception exceptionInner)
                {
                    Log.ForContext<LockItBase>().Error("Authentication failed. Disconnect throw an exception. {Exception}", exceptionInner);
                }

                Log.ForContext<LockItBase>().Error($"Auth failed for device name={lockIt.Name}, GUID={lockIt.Guid}.");

                throw;
            }

            lockIt.CopriKey = authInfo.K_u;

            Log.ForContext<LockItBase>().Debug($"Auth succeeded for device name={lockIt.Name}, GUID={lockIt.Guid}, state={lockIt.GetDeviceState()}.");
            return lockIt;
        }

        /// <summary> Performs an authentication.</summary>
        private static async Task AuthenticateAsync(
            LockItBase lockIt,
            LockInfoAuthTdo lockInfo,
            ICipher cipher)
        {
            Log.ForContext<LockItBase>().Debug($"Request to autenticate for {lockIt.Name}.");

            var authCharacteristic = await lockIt.GetAuthCharacteristicAsync();
            if (authCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Debug("Getting auth-charcteristic failed.");
                throw new CoundntGetCharacteristicException("Authentication failed. Auth characteristic must not be null.");
            }

            bool success;

            try
            {
                success = await authCharacteristic.WriteAsync(lockInfo.K_seed);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed), exception);
                throw new System.Exception(string.Format("Can not authenticate. Writing copri seed failed. {0}", exception.Message), exception);
            }
            if (!success)
            {
                Log.ForContext<LockItBase>().Debug("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed));
                throw new System.Exception("Can not authenticate. Writing copri seed did not succeed.");
            }

            Log.ForContext<LockItBase>().Debug("Copri seed written successfully.{AuthCharacteristic}{CommandWritten}.", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed));

            byte[] seedLockEncrypted;
            var cts = new CancellationTokenSource();
            cts.CancelAfter(READ_TIMEOUT_MS);
            try
            {
                seedLockEncrypted = await authCharacteristic.ReadAsync(cts.Token); // encrypted seed value (random value) from lock to decypt using k_user
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieveing encrypted random value from lock (seed)(ReadAsync-call) failed.{ReadCharacteristic}{Exception}", ToSerilogString(authCharacteristic), exception);
                throw new System.Exception(string.Format("Can not authenticate. Reading encrypted seed failed. {0}", exception.Message), exception);
            }
            finally
            {
                cts.Dispose();
            }

            Log.ForContext<LockItBase>().Debug("Retrieveing encrypted random value from lock (seed)(ReadAsync-call) succeeded.{ReadCharacteristic}{Reading}", ToSerilogString(authCharacteristic), "***");

            var crypto = new AuthCryptoHelper(
                seedLockEncrypted,
                lockInfo.K_u,
                cipher);

            byte[] accessKeyEncrypted;
            try
            {
                accessKeyEncrypted = crypto.GetAccessKeyEncrypted();
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Error getting encrypted access key. {Exception}", exception);
                throw new System.Exception(string.Format("Can not authenticate. Getting access key failed. {0}", exception.Message), exception);
            }

            try
            {
                success = await authCharacteristic.WriteAsync(accessKeyEncrypted);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), ToSerilogString(accessKeyEncrypted), exception);
                throw new System.Exception(string.Format("Can not authenticate. Writing access key failed. {0}", exception.Message), exception);
            }
            if (!success)
            {
                Log.ForContext<LockItBase>().Debug("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***");
                throw new System.Exception(string.Format("Can not authenticate. Writing access key did not succeed."));
            }

            Log.ForContext<LockItBase>().Debug("Encrypted access key written successfully.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***");
        }

        /// <summary> Gets the entire lock state, like locking state (open/ close) and GUID. </summary>
        /// <param name="doWaitRetry">True if to wait and retry in case of failures. </param>
        public async Task<LockInfoTdo> GetLockStateAsync(bool doWaitRetry = false)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not get lock state. Bluetooth code must be run on main thread");
            }

            DeviceState? deviceState;
            Log.ForContext<LockItBase>().Debug("Request to get connection state in context of getting locking state.");
            try
            {
                deviceState = GetDeviceState(Device.State);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Can not get lock state. Retrieving bluetooth state failed. {Exception}", exception);
                throw new System.Exception(string.Format("Can not get lock state. Getting bluetooth state failed. {0}", exception.Message), exception);
            }

            switch (deviceState)
            {
                case DeviceState.Disconnected:
                    throw new BluetoothDisconnectedException();

                case DeviceState.Connected:
                    break;

                default:
                    // Can not get state if device is not connected.
                    Log.ForContext<LockItBase>().Error($"Getting lock state failed. Unexpected lock state {deviceState} detected.");
                    throw new System.Exception(string.Format("Can not get lock state. Unexpected bluetooth state {0} detected.", deviceState));
            }

            Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState}.");

            var stateCharacteristic = await GetStateCharacteristicAsync();
            if (stateCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Error($"Can not get lock state. State characteristic is not available.");
                throw new CoundntGetCharacteristicException("Can not get lock state. State characteristic must not be null.");
            }

            byte[] state;
            async Task<byte[]> readAsyncDelegate()
            {
                var cts = new CancellationTokenSource();
                cts.CancelAfter(READ_TIMEOUT_MS);
                try
                {
                    return await stateCharacteristic.ReadAsync(cts.Token);
                }
                catch (System.Exception exception)
                {
                    Log.ForContext<LockItBase>().Error("Retrieving lock state (ReadAsync-call) failed inside delegate.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception);
                    throw;
                }
                finally
                {
                    cts.Dispose();
                }
            }

            try
            {
                state =  doWaitRetry 
                    ? await _retryPollicy.ExecuteAsync(async () => await readAsyncDelegate())
                    : await readAsyncDelegate();
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving lock state (ReadAsync-call) failed.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception);
                throw new System.Exception(string.Format("Can not get lock state. Reading data failed. {0}", exception.Message), exception);
            }
            if (state == null || state.Length <= 0)
            {
                Log.ForContext<LockItBase>().Debug("Retrieving lock state (ReadAsync-call) failed. Data read is null or empty.{StateCharacteristic}", ToSerilogString(stateCharacteristic));
                throw new System.Exception("Can not get lock state. No data read");
            }

            var lockInfoTdo = new LockInfoTdo.Builder
            {
                Id = Id,
                Guid = Guid,
                State = (LockitLockingState?)state[0]
            }.Build();

            Log.ForContext<LockItBase>().Debug("Retrieving lock state (ReadAsync-call) succeeded.{@LockInfoTdo}{StateCharacteristic}{Reading}", lockInfoTdo, ToSerilogString(stateCharacteristic), state);

            return lockInfoTdo;
        }

        /// <summary>Gets the battery percentage.</summary>
        public async Task<double> GetBatteryPercentageAsync()
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not get battery percentage. Bluetooth code must be run on main thread");
            }

            DeviceState? deviceState;
            Log.ForContext<LockItBase>().Debug("Request to get battery percentage in context of getting locking state.");
            try
            {
                deviceState = GetDeviceState(Device.State);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Can not get battery percentage. Retrieving bluetooth state failed. {Exception}", exception);
                throw new System.Exception(string.Format("Can not get battery percentage. Getting bluetooth state failed. {0}", exception.Message), exception);
            }

            switch (deviceState)
            {
                case DeviceState.Disconnected:
                    throw new BluetoothDisconnectedException();

                case DeviceState.Connected:
                    break;

                default:
                    // Can not get state if device is not connected.
                    Log.ForContext<LockItBase>().Error($"Getting battery percentage failed. Unexpected battery percentage {deviceState} detected.");
                    throw new System.Exception(string.Format("Can not get battery percentage. Unexpected bluetooth state {0} detected.", deviceState));
            }

            Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState}.");

            var batteryCharacteristic = await GetBatteryCharacteristicAsync();
            if (batteryCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Error($"Can not get battery percentage. State characteristic is not available.");
                throw new CoundntGetCharacteristicException("Can not get battery percentage. State characteristic must not be null.");
            }

            byte[] state;
            var cts = new CancellationTokenSource();
            cts.CancelAfter(READ_TIMEOUT_MS);
            try
            {
                state = await batteryCharacteristic.ReadAsync(cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving charging level (ReadAsync-call) failed. {BatteryCharacteristic}{Exception}", ToSerilogString(batteryCharacteristic), exception);
                throw new System.Exception(string.Format("Can not get battery percentage. Reading data failed. {0}", exception.Message), exception);
            }
            finally
            {
                cts.Dispose();
            }

            if (state == null || state.Length <= 0)
            {
                Log.ForContext<LockItBase>().Debug("Retrieving charging level (ReadAsync-call) failed. Data read is null or empty.{BatteryCharacteristic}", ToSerilogString(batteryCharacteristic));
                throw new System.Exception("Can not get battery percentage. No data read.");
            }

            Log.ForContext<LockItBase>().Debug("Retrieving charging level (ReadAsync-call) succeeded.{Level}{BatteryCharacteristic}{Reading}", state[0], ToSerilogString(batteryCharacteristic), state);

            return state[0];
        }

        /// <summary> Opens lock. </summary>
        /// <returns> Locking state.</returns>
        public abstract Task<LockitLockingState?> OpenAsync();

        /// <summary> Close the lock.</summary>
        /// <returns>Locking state.</returns>
        public abstract Task<LockitLockingState?> CloseAsync();

        /// <summary> Opens/ closes lock.</summary>
        /// <param name="counter"></param>
        /// <param name="open"></param>
        /// <returns>True if opening/ closing command could be written successfully.</returns>
        protected async Task<bool> OpenCloseLock(bool open)
        {
            DeviceState deviceState;
            Log.ForContext<LockItBase>().Debug("Request to get connection state in context of opening/ closing lock.");
            try
            {
                deviceState = GetDeviceState(Device.State);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving bluetooth state failed when opening/ closing lock failed. {Exception}", exception);
                throw new System.Exception($"Can not Open/ Close lock. Getting bluetooth failed. {exception.Message}", exception);
            }

            switch (deviceState)
            {
                case DeviceState.Disconnected:
                    throw new BluetoothDisconnectedException();

                case DeviceState.Connected:
                    break;

                default:
                    // Can not open lock if bluetooth state is not connected.
                    Log.ForContext<LockItBase>().Debug($"Can not open/ close lock. Unexpected connection state detected {deviceState}.");
                    return false;
            }

            Log.ForContext<LockItBase>().Debug($"Connection state before opening/ closing lock is {deviceState}.");

            var activateLockCharacteristic = await GetActivateLockCharacteristicAsync();
            if (activateLockCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Debug("Getting lock control service failed.");
                return false;
            }

            byte[] state = bitShift(
                new byte[] { 0, 0, open ? (byte)1 : (byte)2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
                ++ActivateLockWriteCounter);

            byte[] stateEnctryped;
            try
            {
                stateEnctryped = Cipher.Encrypt(CopriKey, state);
            }
            catch (System.Exception exception)
            {
                ActivateLockWriteCounter--;
                Log.ForContext<LockItBase>().Error("Encypting command to open/ close lock failed. {Exception}", exception);
                throw new System.Exception($"Can not open/ close lock. Encrypting command to lock/ unlock failed. {exception.Message}", exception);
            }

            bool success;
            try
            {
                success = await activateLockCharacteristic.WriteAsync(stateEnctryped);
            }
            catch (System.Exception exception)
            {
                ActivateLockWriteCounter--;
                Log.ForContext<LockItBase>().Error(open
                        ? "Writing data to open lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}"
                        : "Writing data to close lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}",
                    ToSerilogString(activateLockCharacteristic), ToSerilogString(stateEnctryped), exception);
                throw new System.Exception($"Can not {(open ? "open" : "close")} lock. Writing command to lock/ unlock failed. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug(open
                    ? "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}"
                    : "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}",
                ToSerilogString(activateLockCharacteristic), ToSerilogString(stateEnctryped));

            return success;
        }

        /// <summary> Turns off the alarm.</summary>
        public async Task<bool> GetIsAlarmOffAsync()
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not turn alarm off. Bluetooth code must be run on main thread");
            }

            var alarmCharacteristic = await GetAlarmCharacteristicAsync();
            if (alarmCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Debug("Getting alarm characteristic failed.");
                return false;
            }

            byte[] alarmSettings;
            var cts = new CancellationTokenSource();
            cts.CancelAfter(READ_TIMEOUT_MS);
            try
            {
                alarmSettings = await alarmCharacteristic.ReadAsync(cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Exception}", ToSerilogString(alarmCharacteristic), exception);
                throw new System.Exception($"Can not get whether alarm is off or on {exception.Message}.");
            }
            finally
            {
                cts.Dispose();
            }

            if (alarmSettings == null || alarmSettings.Length < 1)
            {
                Log.ForContext<LockItBase>().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Reading}", ToSerilogString(alarmCharacteristic));
                throw new System.Exception("Can not get whether alarm is off or on. Reading failed.");
            }

            var isAlarmOff = alarmSettings[0] == 0;
            Log.ForContext<LockItBase>().Debug("Retrieving alarm settings (ReadAsync-call) succeeded.{IsArlarmOff}{AlarmCharacteristic}{Reading}", isAlarmOff, ToSerilogString(alarmCharacteristic), alarmSettings);
            return isAlarmOff;
        }

        /// <summary> Sets alarm on or off.</summary>
        public async Task SetIsAlarmOffAsync(bool isActivated)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not turn alarm off. Bluetooth code must be run on main thread");
            }

            ICharacteristic alarmCharacteristic = await GetAlarmCharacteristicAsync();
            if (alarmCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Debug("Getting alarm characteristic failed.");
                throw new System.Exception($"Can not set alarm {isActivated}. Getting alarm characteristic returned null.");
            }

            bool success;
            var command = new byte[] { isActivated ? (byte)1 : (byte)0 };
            try
            {
                success = await alarmCharacteristic.WriteAsync(command);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}{Exception}", ToSerilogString(alarmCharacteristic), command[0], exception);
                throw new System.Exception($"Can not set alarm settings. {exception.Message}", exception);
            }

            if (!success)
            {
                Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}", ToSerilogString(alarmCharacteristic), command[0]);
                throw new System.Exception($"Can not set alarm settings. Writing  settings did not succeed.");
            }

            Log.ForContext<LockItBase>().Debug("Sound settings written successfully.{AlarmCharacteristic}{CommandWritten}.", ToSerilogString(alarmCharacteristic), command[0]);
        }

        public async Task<bool> SetSoundAsync(SoundSettings settings)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not set sound settings. Bluetooth code must be run on main thread");
            }

            ICharacteristic soundCharacteristic = await GetSoundCharacteristicAsync();
            if (soundCharacteristic == null)
            {
                Log.ForContext<LockItBase>().Debug("Getting sound characteristic failed.");
                return false;
            }

            bool success;
            byte command = (byte)settings;
            try
            {
                success = await soundCharacteristic.WriteAsync(new byte[] { command });
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Writing sound settings failed.{SoundCharacteristic}{CommandWritten}{Exception}", ToSerilogString(soundCharacteristic), command, exception);
                throw new System.Exception($"Can not set sound settings. {exception.Message}", exception);
            }

            Log.ForContext<LockItBase>().Debug(success
                    ? "Writing sound settings failed.{SoundCharacteristic}{CommandWritten}"
                    : "Sound setting written successfully.{SoundCharacteristic}{CommandWritten}",
                ToSerilogString(soundCharacteristic), command);

            return success;
        }

        private static byte[] bitShift(byte[] data, int counter)
        {
            int mask = 0x000000FF;
            data[0] = (byte)(counter & mask);
            data[1] = (byte)(counter >> 8);

            return data;
        }

        private static DeviceState GetDeviceState(Plugin.BLE.Abstractions.DeviceState state)
        {
            switch (state)
            {
                case Plugin.BLE.Abstractions.DeviceState.Disconnected:
                    return DeviceState.Disconnected;

                case Plugin.BLE.Abstractions.DeviceState.Connecting:
                    return DeviceState.Connecting;

                case Plugin.BLE.Abstractions.DeviceState.Connected:
                    return DeviceState.Connected;

                case Plugin.BLE.Abstractions.DeviceState.Limited:
                    return DeviceState.Limited;

                default:
                    throw new ArgumentException($"Can not convert state {state}");
            }
        }

        /// <summary> Disconnect from bluetooth lock. </summary>
        public async Task Disconnect() => await Adapter.DisconnectDeviceAsync(Device);

        /// <summary>
        /// Don' t use .Destructure.ByTransforming<ICharacteristic>(...) because this would introduce a dependency to Plugin.BLE in main project.
        /// </summary> 
        /// <param name="charcteristic"></param>
        /// <returns></returns>
        private static string ToSerilogString(ICharacteristic charcteristic)
            => charcteristic.Id.ToString();

        private static string ToSerilogString(byte[] byteArray)
            => "***"; // For debuging purposes it might be reqired to return string.Join(",", byteArray); Do not log any confidental value in production context.
    }
}