using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Model.Connector;
using System;
using Serilog;
using TINK.Model.Device;
using Xamarin.Essentials;
using TINK.Model.Bike.BluetoothLock;

namespace TINK.Services.BluetoothLock.BLE
{
    public abstract class LockItServiceBase
    {
        /// <summary>Constructs base object.</summary>
        /// <param name="cipher">Encrpyting/ decrypting object.</param>        /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
        public LockItServiceBase(ICipher cipher)
        {
            Cipher = cipher;
        }

        /// <summary> Holds timeout values for series of connecting attemps to a lock or multiple locks. </summary>
        public ITimeOutProvider TimeOut { get; set; }

        protected ICipher Cipher { get; }

        /// <summary> List of available or connected bluetooth devices. </summary>
        protected List<ILockService> DeviceList = new List<ILockService>();


        /// <summary> Reconnect locks of interest if required. </summary>
        /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be reconnected. </remarks>
        /// <param name="locksInfo">Locks to reconnect.</param>
        /// <param name="connectTimeout">Timeout for connect operation.</param>
        public static async Task CheckReconnect(
            IEnumerable<ILockService> deviceList,
            IEnumerable<LockInfoAuthTdo> locksInfo, 
            TimeSpan connectTimeout)
        {
            // Get list of target locks without invalid entries.
            var validLocksInfo = locksInfo
                .Where(x => x.IsIdValid || x.IsGuidValid)
                .Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
                .ToList();

            foreach (var device in deviceList)
            {
                if (device.GetDeviceState() == DeviceState.Connected)
                {
                    // No need to reconnect device because device is already connected.
                    continue;
                }

                var lockInfo = validLocksInfo.FirstOrDefault(x => x.Id == device. Name.GetBluetoothLockId() || x.Guid == device.Guid);

                if (lockInfo == null)
                {
                    // Current lock from deviceList is not of interest detected, no need to reconnect.
                    continue;
                }

                try
                {
                    await device.ReconnectAsync(lockInfo, connectTimeout);
                }
                catch (System.Exception exception)
                {
                    // Member is called for background update of missing devices. 
                    // Do not display any error messages.
                    Log.ForContext<LockItServiceBase>().Error($"Reconnect failed. {exception.Message}");
                    continue;
                }
            }
        }

        /// <summary> Gets the state for locks of interest.</summary>
        /// <remarks>
        /// Might require a
        /// - connect to lock 
        /// - reconnect to lock
        /// </remarks>
        /// <param name="locksInfo">Locks toget info for.</param>
        /// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
        public async Task<IEnumerable<LockInfoTdo>> GetLocksStateAsync(
            IEnumerable<LockInfoAuthTdo> locksInfo, 
            TimeSpan connectTimeout)
        {
            if (locksInfo.Count() == 0)
            {
                // Nothing to do.
                return new List<LockInfoTdo>();
            }

            // Reconnect locks.
            await CheckReconnect(DeviceList, locksInfo, connectTimeout);

            // Connect to locks which were not yet discovered.
            DeviceList.AddRange(await CheckConnectMissing(locksInfo, connectTimeout));

            // Get devices for which to update state.            
            var locksInfoState = new List<LockInfoTdo>();

            foreach (var device in DeviceList)
            {
                var lockInfoAuth = locksInfo.FirstOrDefault(x => x.Guid == device.Guid || x.Id == device.Id);
                if (lockInfoAuth == null)
                {
                    // Device is not of interest.
                    continue;
                }

                var state = await device.GetLockStateAsync();
                locksInfoState.Add(new LockInfoTdo.Builder
                {
                    Id = lockInfoAuth.Id,
                    Guid = lockInfoAuth.IsGuidValid ? lockInfoAuth.Guid : device.Guid,
                    State = state?.State
                }.Build());
            }

            return locksInfoState;
        }

        /// <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 abstract Task<IEnumerable<ILockService>> CheckConnectMissing(
            IEnumerable<LockInfoAuthTdo> locksInfo, 
            TimeSpan connectTimeout);

        /// <summary>Gets a lock by bike Id.</summary>
        /// <param name="bikeId"></param>
        /// <returns>Lock object</returns>
        public ILockService this[int bikeId]
        {
            get
            {
                var device = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId);
                if (device == null)
                {
                    return new NullLock(bikeId);
                }

                return device;
            }
        }

        /// <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<LockingState> DisconnectAsync(int bikeId, Guid bikeGuid)
        {
            if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
            {
                throw new System.Exception("Can not disconnect from lock. Bluetooth code must be run on main thread");
            }

            Log.ForContext<LockItByScanServiceBase>().Debug($"Request to disconnect from device {bikeId}/ {bikeGuid}.");

            var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId || x.Guid == bikeGuid);

            if (lockIt == null)
            {
                // Nothing to do
                return LockingState.Disconnected;
            }

            DeviceList.Remove(lockIt);

            if (lockIt.GetDeviceState() == DeviceState.Disconnected)
            {
                // Nothing to do
                return LockingState.Disconnected;
            }

            await lockIt.Disconnect();
            return LockingState.Disconnected;
        }
    }
}