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

namespace TINK.Services.BluetoothLock.BLE
{
    /// <summary> Manages ILockIt- Locks.</summary>
    public abstract class LockItByScanServiceBase : LockItServiceBase
    {
        private Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> AuthenticateDelegate { get; set;  }

        public LockItByScanServiceBase(
            ICipher cipher,
            Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> authenticateDelegate) : base(cipher) 
        {
            AuthenticateDelegate = authenticateDelegate;
        }

        /// <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 name. Bluetooth code must be run on main thread");
            }

            Log.ForContext<LockItByScanServiceBase>().Debug($"Request to connect to device {authInfo.Id}.");

            var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id || x.Guid == authInfo.Guid);

            if (lockIt != null && lockIt.GetDeviceState() == DeviceState.Connected)
            {
                // Nothing to do
                return await lockIt.GetLockStateAsync();
            }

            if (lockIt != null)
            {
                // Reconnect required
                Log.ForContext<LockItByScanServiceBase>().Debug($"Device {lockIt.Name} has been already connected but needs reconnect.");
                await lockIt.ReconnectAsync(authInfo, connectTimeout);
                return await lockIt.GetLockStateAsync();
            }

            // Device has not yet been discovered.
            var newlyDiscovertedDevice = await ScanForNewDevices(new List<int> { authInfo.Id }, connectTimeout);

            var bleDevice = newlyDiscovertedDevice.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id);

            if (bleDevice == null)
            {

                Log.ForContext<LockItByScanServiceBase>().Debug("Can not connect because device was not discovered.");
                throw new OutOfReachException();
            }

            var adapter = CrossBluetoothLE.Current.Adapter;
            var cts = new CancellationTokenSource(connectTimeout);

            // Connect to device and authenticate.
            Log.ForContext<LockItByScanServiceBase>().Debug($"Device sucessfully discovered. Request connect to device {bleDevice?.Name}. Connect state is {bleDevice?.State}.");
            try
            {
                await adapter.ConnectToDeviceAsync(
                    bleDevice,
                    new ConnectParameters(forceBleTransport: true /* Force BLE transport */),
                    cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItBase>().Error("Can not connect to device by name. {Exception}", exception);
                if (exception is TaskCanceledException)
                {
                    // A timeout occurred.
                    throw new System.Exception($"Can not connect to lock by name.\r\nTimeout is {connectTimeout.TotalMilliseconds} [ms].\r\n{exception.Message}", exception);
                }

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

            lockIt = await AuthenticateDelegate(bleDevice, authInfo, CrossBluetoothLE.Current.Adapter);
            if (lockIt == null)
            {
                await adapter.DisconnectDeviceAsync(bleDevice);
                return await Task.FromResult<LockInfoTdo>(null);
            }

            DeviceList.Add(lockIt);

            return await lockIt.GetLockStateAsync();
        }

        /// <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 for each lock to be connected. </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)
        {
            // Get list of target locks without invalid entries.
            var validLocksInfo = locksInfo
                .Where(x => x.IsIdValid)
                .Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
                .ToList();

            // Get Locks to scan for
            var locksInfoToScanFor = validLocksInfo.Where(x => !DeviceList.Any(y => y.Name.GetBluetoothLockId() == x.Id)).ToList();

            // Check if bluetooth scan is required
            if (locksInfoToScanFor.Count <= 0)
            {
                // No Scan required.
                return new List<LockItBase>();
            }

            // Do bluetooth scan
            var scanTargets = locksInfoToScanFor.Select(x => x.Id).ToList();
            var newlyDiscovertedDevicesList = await ScanForNewDevices(scanTargets, TimeSpan.FromSeconds(connectTimeout.TotalSeconds * scanTargets.Count));

            var locksList = new List<LockItBase>();

            // Connect to newly discovered devices.
            foreach (var lockInfo in locksInfoToScanFor)
            {
                var device = newlyDiscovertedDevicesList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == lockInfo.Id); 
                if (device == null)
                {
                    // No device found for given lock.
                    continue;
                }

                Log.ForContext<LockItByScanServiceBase>().Debug($"Request connect to device {device?.Name}. Connect state is {device?.State}.");

                var cts = new CancellationTokenSource(connectTimeout);

                try
                {
                    // Connect to device and authenticate.
                    await CrossBluetoothLE.Current.Adapter.ConnectToDeviceAsync(
                        device,
                        new ConnectParameters(forceBleTransport: true), // Force BLE transport 
                        cts.Token);
                }
                catch (System.Exception exception)
                {
                    Log.ForContext<LockItBase>().Error("Can not connect to lock. {Exception}", exception);
                    continue;
                }

                LockItBase lockIt = null;
                try
                {
                    lockIt = await AuthenticateDelegate(device, lockInfo, CrossBluetoothLE.Current.Adapter);
                }
                catch (System.Exception exception)
                {
                    // Member is called for background update of missing devices. 
                    // Do not display any error messages.
                    Log.ForContext<LockItByScanServiceBase>().Error($"Authentication failed. {exception.Message}");
                    continue;
                }
                if (lockIt == null)
                {
                    // connectiong to device succeded.
                    continue;
                }

                Log.ForContext<LockItByScanServiceBase>().Debug($"Auth succeeded for device {device}.");
                locksList.Add(lockIt);
            }

            return locksList;
        }

        /// <summary> Scans for a given set of devices.</summary>
        /// <param name="targetLocks">Locks to scan for.</param>
        /// <returns></returns>
        private static async Task<IEnumerable<IDevice>> ScanForNewDevices(
            IEnumerable<int> targetLocks,
            TimeSpan connectTimeout)
        {
            var adapter = CrossBluetoothLE.Current.Adapter;

            var newlyDiscovertedDevicesList = new List<IDevice>();

            var cts = new CancellationTokenSource(connectTimeout);

            // Not all devices already connected.
            // Start scanning
            adapter.DeviceDiscovered += (s, a) =>
            {
                var name = a.Device.Name;
                if (string.IsNullOrEmpty(name))
                {
                    // Lock without name detected.
                    Log.ForContext<LockItByScanServiceBase>().Debug($"Lock without name discovered.");
                    return;
                }

                if (targetLocks.Where(x => x != TextToLockItTypeHelper.INVALIDLOCKID).Contains(name.GetBluetoothLockId()))
                {
                    Log.ForContext<LockItByScanServiceBase>().Debug($"New LOCKIT device {name} discovered.");

                    newlyDiscovertedDevicesList.Add(a.Device);

                    if (newlyDiscovertedDevicesList.Count() >= targetLocks.Count())
                    {
                        cts.Cancel();
                    }

                    return;
                }

                Log.ForContext<LockItByScanServiceBase>().Verbose($"Device of unknown advertisement name {name} discovered.");
            };

            try
            {
                await adapter.StartScanningForDevicesAsync(cancellationToken: cts.Token);
            }
            catch (System.Exception exception)
            {
                Log.ForContext<LockItByScanServiceBase>().Error("Bluetooth scan failed. {Exception}", exception);
                throw;
            }
            finally
            {
                try
                {
                    await adapter.StopScanningForDevicesAsync();
                }
                catch (System.Exception exception)
                {
                    Log.ForContext<LockItByScanServiceBase>().Error("Stop scanning failed. {Excpetion}", exception);
                    throw;
                }
            }
            
            return newlyDiscovertedDevicesList;
        }
    }
}