using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using GalaxyBudsClient.Platform.Interfaces;
using GalaxyBudsClient.Platform.Model;
using Serilog;
using ThePBone.BlueZNet;
using ThePBone.BlueZNet.Interop;
using Tmds.DBus;

namespace GalaxyBudsClient.Platform.Linux
{
    public class BluetoothService : IBluetoothService
    {
        private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(15);
        private static readonly ConcurrentQueue<byte[]> TransmitterQueue = new();
        
        private CancellationTokenSource _cancelSource = new();
        private readonly BluetoothSocket _profile = new();
        private Task? _loop;
        
        private IAdapter1? _adapter;
        private IDevice1? _device;

        private string _currentMac = string.Empty;
        private string _currentUuid = string.Empty;

        private IDisposable? _connectionWatchdog;

        private IDisposable? ConnectionWatchdog
        {
            get => _connectionWatchdog;
            set
            {
                /* Update connection watcher */
                _connectionWatchdog?.Dispose();
                _connectionWatchdog = value;
            }
        }
        
        public event EventHandler? RfcommConnected;
        public event EventHandler? Connecting;
        public event EventHandler? Connected;
        public event EventHandler<string>? Disconnected;
        public event EventHandler<BluetoothException>? BluetoothErrorAsync;
        public event EventHandler<byte[]>? NewDataAvailable;

        public bool IsStreamConnected { get; set; }

        public BluetoothService()
        {
            RfcommConnected += (sender, args) => IsStreamConnected = true;
            Disconnected += (sender, args) => IsStreamConnected = false;
        }

        #region Adapter
        public async Task SelectAdapter(string preferred = "")
        {
            if(preferred.Length <= 0)
            {
                preferred = Environment.GetEnvironmentVariable("BT_ADAPTER") ?? string.Empty;
            }

            try
            {
                if (preferred.Length > 0)
                {
                    try
                    {
                        _adapter = await BlueZManager.GetAdapterAsync(preferred);
                    }
                    catch (BlueZException ex)
                    {
                        Log.Warning("Preferred adapter not available: {ErrorName}", ex.ErrorName);
                        _adapter = null;
                    }
                    catch (DBusException ex)
                    {
                        BluetoothErrorAsync?.Invoke(this,
                            new BluetoothException(BluetoothException.ErrorCodes.NoAdaptersAvailable,
                                $"BlueZ not available. Make sure you've installed BlueZ and enabled that driver properly. {ex.Message}"));
                        return;
                    }
                }

                if (_adapter == null || preferred.Length == 0)
                {
                    var adapters = await BlueZManager.GetAdaptersAsync();
                    if (adapters.Count == 0)
                    {
                        throw new BluetoothException(BluetoothException.ErrorCodes.NoAdaptersAvailable);
                    }

                    _adapter = adapters.First();
                }

                var adapterPath = _adapter.ObjectPath.ToString();
                var adapterName = adapterPath.Substring(adapterPath.LastIndexOf("/", StringComparison.Ordinal) + 1);

                Log.Debug("Linux.BluetoothService: Using Bluetooth adapter: {AdapterName}", adapterName);
            }
            catch (DBusException ex)
            {
                Log.Error(ex, "Failed to select adapter");
                if (ex.ErrorName is "org.freedesktop.DBus.Error.TimedOut" or "org.freedesktop.DBus.Error.NameHasNoOwner")
                {
                    throw new BluetoothException(BluetoothException.ErrorCodes.NoAdaptersAvailable,
                        "BlueZ not available. Make sure you've installed BlueZ and enabled that driver properly.");
                }
            }
        }
        #endregion

        #region Connection
        public async Task ConnectAsync(string macAddress, string uuid, CancellationToken cancelToken)
        {
            Connecting?.Invoke(this, EventArgs.Empty);
            
            if (_adapter == null)
            {
                Log.Debug("Linux.BluetoothService: No adapter preselected. Choosing default one");
                await SelectAdapter();
            }
            
            _device = await _adapter.GetDeviceAsync(macAddress);
            if (_device == null)
            {
                Disconnected?.Invoke(this, 
                    $"Bluetooth peripheral with address '{macAddress}' not found. Use `bluetoothctl` or Bluetooth Manager to scan and possibly pair first.");
                return;
            }

            _currentMac = macAddress;
            _currentUuid = uuid;
            
            var conn = new Connection(Address.System);
            await conn.ConnectAsync();
            
            _profile.NewConnection = (path, handle, arg3) => OnConnectionEstablished();
            _profile.RequestDisconnection = async (path, handle) => await DisconnectAsync();
            await conn.RegisterObjectAsync(_profile);

            for (var attempt = 1; attempt <= 5; attempt++)
            {
                if(cancelToken.IsCancellationRequested)
                    return;
                
                Log.Debug("Linux.BluetoothService: Connecting... (attempt {Attempt}/5)", attempt);
                try
                {
                    if (await AttemptBasicConnectionAsync(cancelToken))
                    {
                        break;
                    }
                }
                catch(BluetoothException ex)
                {
                    BluetoothErrorAsync?.Invoke(this, ex);
                }

                await Task.Delay(50, cancelToken);

                if (attempt >= 5)
                {
                    Log.Error("Linux.BluetoothService: Gave up after 5 attempts. Timed out");
                    throw new BluetoothException(BluetoothException.ErrorCodes.TimedOut, "BlueZ timed out while connecting to device.");
                }
            }

            try
            {
                await _device.WaitForPropertyValueAsync("Connected", value: true, Timeout, cancelToken);
            }
            catch (TimeoutException)
            {
                Log.Error("Linux.BluetoothService: Timed out while waiting for device to connect");
                throw new BluetoothException(BluetoothException.ErrorCodes.TimedOut, "BlueZ timed out while connecting to device.");
            }

            Connected?.Invoke(this, EventArgs.Empty);
            ConnectionWatchdog = _device.WatchForPropertyChangeAsync("Connected", true, ConnectionWatcherCallback);
            Log.Debug("Linux.BluetoothService: Device ready. Registering profile client for UUID {Uuid}...", uuid);

            var properties = new Dictionary<string, object>
            {
                ["Role"] = "client", 
                ["Service"] = uuid, 
                ["Name"] = "GalaxyBudsClient"
            };

            var profileManager = conn.CreateProxy<IProfileManager1>(BluezConstants.DbusService, "/org/bluez");

            try
            {
                await profileManager.UnregisterProfileAsync(_profile.ObjectPath);
            }
            catch (Exception e)
            {
                Log.Information("Linux.BluetoothService: Unregistering profile failed: {ErrorMessage}", e.Message);
            }
            
            try
            {
                await profileManager.RegisterProfileAsync(_profile.ObjectPath, uuid, properties);
            }
            catch (DBusException e)
            {
                var ex = new BlueZException(e);

                switch (ex.ErrorCode)
                {
                    case BlueZException.ErrorCodes.AlreadyExists:
                        Log.Warning("Linux.BluetoothService:  UUID already registered. This may be fatal when multiple instances are active");
                        break;
                    case BlueZException.ErrorCodes.NotPermitted:
                        if(ex.ErrorMessage.Contains("UUID already registered"))
                        {
                            Log.Warning("Linux.BluetoothService: UUID already registered. This may be fatal when multiple instances are active");
                            break;
                        }
                        
                        Log.Warning("Linux.BluetoothService: Not permitted. Cannot register profile: {ExErrorMessage}", ex.ErrorMessage);
                        break;
                    case BlueZException.ErrorCodes.InvalidArguments:
                        Log.Error("Linux.BluetoothService: Invalid arguments. Cannot register profile: {ExErrorMessage}", ex.ErrorMessage);
                        throw new BluetoothException(BluetoothException.ErrorCodes.Unknown, $"{ex.ErrorName}: {ex.ErrorMessage}");
                    default:
                        /* Other unknown dbus errors */
                        Log.Error("Linux.BluetoothService: Cannot register profile. {ExErrorName}: {ExErrorMessage}", ex.ErrorName, ex.ErrorMessage);
                        throw new BluetoothException(BluetoothException.ErrorCodes.Unknown, $"{ex.ErrorName}: {ex.ErrorMessage}");
                }
            }
            
            for (var attempt = 1; attempt <= 10; attempt++)
            {
                if(cancelToken.IsCancellationRequested)
                    return;
                
                Log.Debug("Linux.BluetoothService: Connecting to profile... (attempt {Attempt}/10)", attempt);

                try
                {
                    await _device.ConnectProfileAsync(uuid);
                    break;
                }
                catch (DBusException e)
                {
                    var ex = new BlueZException(e);

                    switch (ex.ErrorCode)
                    {
                        case BlueZException.ErrorCodes.Failed:
                            Log.Debug("Linux.BluetoothService: Failed: \'{ExErrorMessage}\'", ex.ErrorMessage);
                            await Task.Delay(500, cancelToken);
                            break;
                        case BlueZException.ErrorCodes.InProgress:
                            Log.Debug("Linux.BluetoothService: Already connecting");
                            await Task.Delay(500, cancelToken);
                            break;
                        case BlueZException.ErrorCodes.AlreadyConnected:
                            Log.Debug("Linux.BluetoothService: Success. Already connected");
                            return; /* We return here */
                        case BlueZException.ErrorCodes.ConnectFailed:
                            throw new BluetoothException(BluetoothException.ErrorCodes.ConnectFailed, $"{ex.ErrorName}: {ex.ErrorMessage}");
                        case BlueZException.ErrorCodes.DoesNotExist:
                            Log.Error("Linux.BluetoothService: Unsupported device. Device does not provide requested Bluetooth profile");
                            throw new BluetoothException(BluetoothException.ErrorCodes.UnsupportedDevice, $"Device does not provide required Bluetooth profile");
                        default:
                            /* Other unknown dbus errors */
                            Log.Error("Linux.BluetoothService: Cannot connect to profile. {ExErrorName}: {ExErrorMessage}", ex.ErrorName, ex.ErrorMessage);
                            throw new BluetoothException(BluetoothException.ErrorCodes.Unknown, $"{ex.ErrorName}: {ex.ErrorMessage}");
                    }
                }

                if (attempt >= 10)
                {
                    Log.Error("Linux.BluetoothService: Gave up after 10 attempts. Timed out");
                    throw new BluetoothException(BluetoothException.ErrorCodes.TimedOut, "BlueZ timed out while connecting to profile");
                }
            }
        }

        private async void ConnectionWatcherCallback(bool state)
        {
            if (state)
            {
                Connected?.Invoke(this, EventArgs.Empty);
                Log.Debug("Linux.BluetoothService: Reconnected. Attempting to auto-connect to profile...");
                try
                {
                    await ConnectAsync(_currentMac, _currentUuid, CancellationToken.None);
                }
                catch (BluetoothException ex)
                {
                    BluetoothErrorAsync?.Invoke(this, ex);
                }
            }
            else
            {
                Disconnected?.Invoke(this, "Disconnected");
                Log.Debug("Linux.BluetoothService: Disconnected");
            }
        }

        private async Task<bool> AttemptBasicConnectionAsync(CancellationToken cancelToken)
        {
            if (await _device.GetConnectedAsync())
                return true;

            try
            {
                await _device?.ConnectAsync()!;
            }
            catch (DBusException e)
            {
                var ex = new BlueZException(e);
                switch (ex.ErrorCode)
                {    
                    case BlueZException.ErrorCodes.Failed:
                        Log.Debug("Linux.BluetoothService: Failed: \'{ExErrorMessage}\'", ex.ErrorMessage);
                        
                        if (ex.ErrorMessage.Contains("Host is down", StringComparison.OrdinalIgnoreCase))
                        {
                            throw new BluetoothException(BluetoothException.ErrorCodes.ConnectFailed, $"Failed to connect: '{ex.ErrorMessage}'");
                        }
                        
                        await Task.Delay(250, cancelToken);
                        return false;
                    
                    case BlueZException.ErrorCodes.InProgress:
                        Log.Debug("Linux.BluetoothService: Already connecting");
                        
                        await Task.Delay(500, cancelToken);
                        return false;
                        
                    case BlueZException.ErrorCodes.AlreadyConnected:
                        Log.Debug("Linux.BluetoothService: Already connected. Skipping ahead...");
                        break;
                    default:
                        /* org.bluez.Error.NotReady, org.bluez.Error.Failed */
                        Log.Error("Linux.BluetoothService: Connect call failed due to: {ExErrorMessage} ({ExErrorCode})", ex.ErrorMessage, ex.ErrorCode);
                        throw new BluetoothException(BluetoothException.ErrorCodes.Unknown, $"{ex.ErrorName}: {ex.ErrorMessage}");
                }
            }

            return true;
        }
        
        private void OnConnectionEstablished()
        {
            Log.Debug("Linux.BluetoothService: Connection established. Launching BluetoothServiceLoop");

            try
            {
                _loop?.Dispose();
            }
            catch (InvalidOperationException) {}

            _cancelSource = new CancellationTokenSource();
            _loop = Task.Run(BluetoothServiceLoop, _cancelSource.Token);
                
            RfcommConnected?.Invoke(this, EventArgs.Empty);
        }
        #endregion     
        
        #region Disconnection
        public async Task DisconnectAsync()
        {
            Log.Debug("Linux.BluetoothService: Disconnecting...");
            if (_loop == null || _loop.Status == TaskStatus.Created)
            {
                Log.Debug("Linux.BluetoothService: BluetoothServiceLoop not yet launched. No need to cancel");
            }
            else
            {  
                Log.Debug("Linux.BluetoothService: Cancelling BluetoothServiceLoop...");
                await _cancelSource.CancelAsync();
            }

            /* Disconnect device if not already done... */
            if (_device != null)
            {
                try
                {
                    await _device.DisconnectProfileAsync(_currentUuid);
                    Log.Debug("Linux.BluetoothService: Profile disconnected");
                }
                catch (DBusException ex)
                {
                    Log.Warning("Linux.BluetoothService: (Non-critical) Exception raised while disconnecting profile: {ExErrorName}: {ExErrorMessage}", ex.ErrorName, ex.ErrorMessage);
                    /* Discard non-critical exceptions. */
                }
            }

            /* Attempt to unregister profile if not already done... */
            var profileManager = Connection.System.CreateProxy<IProfileManager1>(BluezConstants.DbusService, "/org/bluez");
            try
            {
                await profileManager.UnregisterProfileAsync(_profile.ObjectPath);
                Log.Debug("Linux.BluetoothService: Profile unregistered");
            }
            catch (DBusException ex)
            {
                Log.Warning("Linux.BluetoothService: (Non-critical) Exception raised while unregistering profile: {ExErrorName}: {ExErrorMessage}", ex.ErrorName, ex.ErrorMessage);
                /* Discard non-critical exceptions. */
            }
        }
        #endregion

        #region Transmission
        public async Task SendAsync(byte[] data)
        {
            lock (TransmitterQueue)
            {
                TransmitterQueue.Enqueue(data);
            }
            await Task.CompletedTask;
        }

        public async Task<BluetoothDevice[]> GetDevicesAsync()
        {
            if (_adapter == null)
            {
                await SelectAdapter();
            }
            
            var devicesBluez = await _adapter.GetDevicesAsync();

            if (devicesBluez == null)
            {
                return Array.Empty<BluetoothDevice>();
            }
            
            var devices = new BluetoothDevice[devicesBluez.Count];
            for (var i = 0; i < devicesBluez.Count; i++)
            {
                var props = await devicesBluez[i].GetAllAsync();
                var uuids = props.UUIDs.Select(u => new Guid(u));
                devices[i] = new BluetoothDevice(props.Name, props.Address, props.Connected,
                    props.Paired, new BluetoothCoD(props.Class), uuids.ToArray());
            }

            return devices;
        }
        #endregion
        
        #region Service
        private async void BluetoothServiceLoop()
        {
            while (true)
            {
                try
                {
                    _cancelSource.Token.ThrowIfCancellationRequested();
                }
                catch (OperationCanceledException)
                {
                    return;
                }

                bool hasIoActivity;
                try
                {
                    var incomingCount = _profile.Stream?.AvailableBytes;
                    if (incomingCount == null)
                    {
                        /* Stream not yet ready */
                        await Task.Delay(100);
                        continue;
                    }

                    if (incomingCount > 0)
                    {
                        IsStreamConnected = true;
                        
                        /* Handle incoming stream */
                        var buffer = new byte[incomingCount ?? 0];
                        var dataAvailable = false;
                        try
                        {
                            dataAvailable = _profile.Stream?.Read(buffer, 0, buffer.Length) >= 0;
                        }
                        catch (UnixSocketException ex)
                        {
                            Log.Error("Linux.BluetoothService: BluetoothServiceLoop: SocketException thrown while reading unsafe stream: {ExMessage}. Cancelled", ex.Message);
                            Disconnected?.Invoke(this, ex.Message);
                            throw;
                        }

                        if (dataAvailable)
                        {
                            NewDataAvailable?.Invoke(this, buffer);
                        }
                    }

                    /* Handle outgoing stream */
                    lock (TransmitterQueue)
                    {
                        if (TransmitterQueue.IsEmpty || !TransmitterQueue.TryDequeue(out var raw))
                        {
                            hasIoActivity = false;
                        }
                        else
                        {
                            hasIoActivity = true;
                            
                            try
                            {
                                _profile.Stream?.Write(raw, 0, raw.Length);
                            }
                            catch (SocketException ex)
                            {
                                Log.Error(
                                    "Linux.BluetoothService: BluetoothServiceLoop: SocketException thrown while writing unsafe stream: {ExMessage}. Cancelled",
                                    ex.Message);
                                Disconnected?.Invoke(this, ex.Message);
                            }
                            catch (IOException ex)
                            {
                                if (ex.InnerException != null && ex.InnerException.GetType() == typeof(SocketException))
                                {
                                    Log.Error(
                                        "Linux.BluetoothService: BluetoothServiceLoop: IO and SocketException thrown while writing unsafe stream: {ExMessage}. Cancelled",
                                        ex.Message);
                                    Disconnected?.Invoke(this, ex.Message);
                                }
                            }
                        }
                    }
                }
                catch (UnixException ex)
                {
                    Log.Error("Linux.BluetoothService: BluetoothServiceLoop: UnixException thrown while handling unsafe stream: {ExMessage}. Cancelled", ex.Message);

                    if (ex.Errno == 104) // Connection reset by peer
                    {
                        Disconnected?.Invoke(this, "Connection reset by peer");
                        return;
                    }
                    
                    BluetoothErrorAsync?.Invoke(this, new BluetoothException(BluetoothException.ErrorCodes.Unknown, ex.Message));
                    return;
                }

                if (!hasIoActivity)
                {
                    await Task.Delay(50);
                }
            }
        }
        #endregion
    }
}
