using System.Collections.Generic;
using System.Threading.Tasks;
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions;
using System.Linq;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Model.Connector;
using Serilog;
using System.Threading;
using System;
using TINK.Services.BluetoothLock.Exception;
using Xamarin.Essentials;
using TINK.Model.Device;
using LockItShared.Services.BluetoothLock;

namespace TINK.Services.BluetoothLock.BLE
{
    public class LockItByGuidService : LockItServiceBase, ILocksService
    {
        /// <summary> Constructs a LockItByGuidService object.</summary>
        /// <param name="cipher">Encrpyting/ decrypting object.</param>
        public LockItByGuidService(ICipher cipher) : base(cipher) 
        {
        }

        /// <summary> Checks for locks which have not yet been discoverted and connects them. </summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="locksInfo">Locks to reconnect.</param>
        /// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
        protected override async Task<IEnumerable<ILockService>> CheckConnectMissing(IEnumerable<LockInfoAuthTdo> locksInfo, TimeSpan connectTimeout)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not connect to locks by guid. Bluetooth code must be run on main thread");
            }

            // Get list of target locks without invalid entries.
            var validLocksInfo = locksInfo
                .Where(x => x.IsGuidValid)
                .Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
                .ToList();

            var locksList = new List<ILockService>();

            // Connect to 
            foreach (var lockInfo in validLocksInfo)
            {
                if (DeviceList.Any(x => x.Name.GetBluetoothLockId() == lockInfo.Id || x.Guid == lockInfo.Guid))
                {
                    // Device is already connected.
                    continue;
                }

                // Connect to device and authenticate.
                ILockService lockIt = null;
                try
                {
                    lockIt = await ConnectByGuid(lockInfo, connectTimeout);
                }
                catch (System.Exception exception)
                {
                    // Member is called for background update of missing devices. 
                    // Do not display any error messages.
                    Log.ForContext<LockItByGuidService>().Error($"Authentication failed. {exception.Message}");
                    continue;
                }
                if (lockIt == null)
                {
                    continue;
                }

                locksList.Add(lockIt);
            }

            return locksList;
        }

        /// <summary> Connects to lock.</summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="authInfo"> Info required to connect to lock.</param>
        /// <param name="connectTimeout">Timeout for connect operation.</param>
        public async Task<LockInfoTdo> ConnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not connect to lock by guid. Bluetooth code must be run on main thread");
            }

            // Connect to device and authenticate.
            var lockIt = await ConnectByGuid(authInfo, connectTimeout);

            if (lockIt == null)
            {
                return new LockInfoTdo.Builder { Id = authInfo.Id, Guid = authInfo.Guid, State = null }.Build();
            }

            DeviceList.Add(lockIt);

            return await lockIt.GetLockStateAsync();
        }

        /// <summary> Connects to lock.</summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
        /// <param name="authInfo"> Info required to connect to lock.</param>
        /// <param name="connectTimeout">Timeout for connect operation.</param>
        private async Task<ILockService> ConnectByGuid(LockInfoAuthTdo authInfo, TimeSpan connectTimeout)
        {
            if (authInfo.Guid == TextToLockItTypeHelper.INVALIDLOCKGUID)
            {
                Log.ForContext<LockItByGuidService>().Error($"Can not connect to lock {authInfo.Id}. Guid is unknown.");
                throw new GuidUnknownException();
            }

            var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id || x.Guid == authInfo.Guid);
            if (lockIt != null && lockIt.GetDeviceState() == DeviceState.Connected)
            {
                // Device is already connected.
                return lockIt;
            }

            var adapter = CrossBluetoothLE.Current.Adapter;
            Log.ForContext<LockItByGuidService>().Debug($"Request connect to device {authInfo.Id}.");

            if (LockItByGuidServiceHelper.DevelGuids.ContainsKey(authInfo.Id) && LockItByGuidServiceHelper.DevelGuids[authInfo.Id] != authInfo.Guid)
                throw new System.Exception($"Invalid Guid {authInfo.Guid} for lock with id {authInfo.Id} detected. Guid should be {LockItByGuidServiceHelper.DevelGuids[authInfo.Id]}.");
            
            IDevice device;
            var cts = new CancellationTokenSource(connectTimeout);
            // Step 1: Perform bluetooth connect.
            try
            {
                device = await adapter.ConnectToKnownDeviceAsync(
                    authInfo.Guid,
                    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 connect to lock by guid.\r\nTimeout of {connectTimeout.TotalMilliseconds} [ms] elapsed.", exception);
                }

                Log.ForContext<LockItByGuidService>().Error("Bluetooth connect to known device request failed. {Exception}", exception);
                throw new System.Exception($"Can not connect to lock by guid.\r\n{exception.Message}", exception);
            }

            // Step 2: Authenticate.
            lockIt = await LockItEventBased.Authenticate(device, authInfo, CrossBluetoothLE.Current.Adapter, Cipher);

            if (lockIt == null)
            {
                Log.ForContext<LockItByGuidService>().Error("Connect to device failed.");
                return null;
            }

            return lockIt;
        }
    }
}