"""
🍎 macOS BLE Server using CoreBluetooth.

Uses pyobjc to interface with CoreBluetooth for BLE peripheral mode.
"""

import asyncio
import logging
from typing import Callable, Awaitable, Optional

logger = logging.getLogger(__name__)

COREBLUETOOTH_AVAILABLE = False

try:
    import objc
    from Foundation import NSObject, NSData
    from CoreBluetooth import (
        CBPeripheralManager,
        CBMutableService,
        CBMutableCharacteristic,
        CBCharacteristicPropertyWrite,
        CBCharacteristicPropertyWriteWithoutResponse,
        CBCharacteristicPropertyNotify,
        CBCharacteristicPropertyRead,
        CBAttributePermissionsWriteable,
        CBAttributePermissionsReadable,
        CBPeripheralManagerStatePoweredOn,
        CBPeripheralManagerStatePoweredOff,
        CBPeripheralManagerStateUnauthorized,
        CBPeripheralManagerStateUnsupported,
        CBUUID,
        CBAdvertisementDataServiceUUIDsKey,
        CBAdvertisementDataLocalNameKey,
    )
    COREBLUETOOTH_AVAILABLE = True
except ImportError as e:
    logger.debug(f"CoreBluetooth not available: {e}")


class MacOSBLEServer:
    """
    BLE GATT Server implementation for macOS using CoreBluetooth.
    """
    
    def __init__(
        self,
        config,  # BLEConfig
        on_message: Optional[Callable[[str], Awaitable[None]]] = None,
    ):
        self.config = config
        self.on_message = on_message
        self._manager = None
        self._delegate = None
        self._service = None
        self._write_char = None
        self._notify_char = None
        self._running = False
        self._loop = None
        self._runloop_task = None
        self._notification_queue = None
        self._notification_task = None
    
    async def start(self) -> bool:
        """Start the macOS BLE peripheral."""
        if not COREBLUETOOTH_AVAILABLE:
            logger.error("CoreBluetooth not available")
            return False
        
        self._loop = asyncio.get_event_loop()
        
        # Create the delegate using informal protocol approach
        self._delegate = _create_peripheral_delegate(self)
        
        # Create the peripheral manager
        self._manager = CBPeripheralManager.alloc().initWithDelegate_queue_(
            self._delegate, None
        )
        
        self._running = True
        logger.info("🍎 macOS BLE peripheral manager created")
        logger.info("⏳ Waiting for Bluetooth to power on...")
        
        # Start CFRunLoop processing in background
        # CoreBluetooth delegate callbacks require CFRunLoop to dispatch events
        self._runloop_task = asyncio.create_task(self._run_cfrunloop())
        
        # Start notification queue processor
        self._notification_queue = asyncio.Queue()
        self._notification_task = asyncio.create_task(self._process_notifications())
        
        return True
    
    async def _run_cfrunloop(self) -> None:
        """
        Process CFRunLoop events periodically.
        
        CoreBluetooth uses CFRunLoop for delegate callbacks. We need to 
        periodically run the loop to process pending events.
        """
        try:
            from PyObjCTools import AppHelper
            from Foundation import NSRunLoop, NSDate
            
            while self._running:
                # Process pending CFRunLoop events (non-blocking)
                # This allows CoreBluetooth delegate methods to be called
                NSRunLoop.currentRunLoop().runMode_beforeDate_(
                    "kCFRunLoopDefaultMode",
                    NSDate.dateWithTimeIntervalSinceNow_(0.02)
                )
                # Yield to asyncio event loop
                await asyncio.sleep(0.01)
        except asyncio.CancelledError:
            pass
        except Exception as e:
            logger.error(f"CFRunLoop error: {e}")
    
    async def stop(self) -> None:
        """Stop the macOS BLE peripheral."""
        self._running = False
        
        # Cancel tasks
        for task in [self._runloop_task, self._notification_task]:
            if task:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass
        
        if self._manager:
            self._manager.stopAdvertising()
            if self._service:
                self._manager.removeService_(self._service)
        logger.info("🍎 macOS BLE peripheral stopped")
    
    def send_notification(self, text: str) -> bool:
        """
        Queue a notification to be sent.
        
        Args:
            text: Text to send.
        
        Returns:
            True if queued successfully.
        """
        if not self._notification_queue:
            return False
            
        try:
            self._notification_queue.put_nowait(text)
            return True
        except Exception as e:
            logger.error(f"Queue error: {e}")
            return False

    async def _process_notifications(self) -> None:
        """Process the notification queue."""
        while self._running:
            try:
                text = await self._notification_queue.get()
                
                # Send logic
                if self._manager and self._notify_char:
                    from Foundation import NSData
                    data = text.encode('utf-8')
                    ns_data = NSData.dataWithBytes_length_(data, len(data))
                    
                    # Try to send with retries
                    sent = False
                    retries = 0
                    while not sent and retries < 10 and self._running:
                        sent = self._manager.updateValue_forCharacteristic_onSubscribedCentrals_(
                            ns_data,
                            self._notify_char,
                            None
                        )
                        if sent:
                            logger.info(f"📲 Notified: {text[:50]}...")
                        else:
                            # Buffer full, wait a bit
                            # logger.debug(f"📲 Buffer full, retrying ({retries+1}/10)...")
                            await asyncio.sleep(0.02 + (retries * 0.02)) # Backoff
                            retries += 1
                
                self._notification_queue.task_done()
                
                # Minimum delay between messages to prevent saturation
                await asyncio.sleep(0.01)
                
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Notification processing error: {e}")
    
    def _setup_service(self) -> None:
        """Set up the GATT service and characteristics."""
        # Create service UUID
        service_uuid = CBUUID.UUIDWithString_(self.config.service_uuid)
        
        # Create write characteristic
        write_uuid = CBUUID.UUIDWithString_(self.config.write_char_uuid)
        self._write_char = CBMutableCharacteristic.alloc().initWithType_properties_value_permissions_(
            write_uuid,
            CBCharacteristicPropertyWrite | CBCharacteristicPropertyWriteWithoutResponse,
            None,
            CBAttributePermissionsWriteable,
        )
        
        # Create notify characteristic
        notify_uuid = CBUUID.UUIDWithString_(self.config.notify_char_uuid)
        self._notify_char = CBMutableCharacteristic.alloc().initWithType_properties_value_permissions_(
            notify_uuid,
            CBCharacteristicPropertyNotify | CBCharacteristicPropertyRead,
            None,
            CBAttributePermissionsReadable,
        )
        
        # Create service
        self._service = CBMutableService.alloc().initWithType_primary_(
            service_uuid, True
        )
        self._service.setCharacteristics_([self._write_char, self._notify_char])
        
        # Add service to peripheral manager
        self._manager.addService_(self._service)
        logger.info("✅ GATT service and characteristics created")
    
    def _start_advertising(self) -> None:
        """Start BLE advertising."""
        service_uuid = CBUUID.UUIDWithString_(self.config.service_uuid)
        advertisement_data = {
            CBAdvertisementDataLocalNameKey: self.config.device_name,
            CBAdvertisementDataServiceUUIDsKey: [service_uuid],
        }
        self._manager.startAdvertising_(advertisement_data)
        logger.info(f"📡 Advertising as '{self.config.device_name}'")
    
    def _handle_write(self, data: bytes) -> None:
        """Handle incoming write from mobile device."""
        try:
            text = data.decode('utf-8')
            logger.info(f"📥 Received via BLE: {text}")
            
            if self.on_message and self._loop:
                asyncio.run_coroutine_threadsafe(
                    self.on_message(text),
                    self._loop
                )
        except Exception as e:
            logger.error(f"Error handling write: {e}")
    



def _create_peripheral_delegate(server: MacOSBLEServer):
    """
    Create a CBPeripheralManagerDelegate using pyobjc's informal protocol pattern.
    
    CoreBluetooth delegate protocols are informal protocols in pyobjc,
    so we create a class that implements the delegate methods.
    """
    if not COREBLUETOOTH_AVAILABLE:
        return None
    
    class PeripheralManagerDelegate(NSObject):
        """Delegate for CBPeripheralManager events."""
        
        def init(self):
            self = objc.super(PeripheralManagerDelegate, self).init()
            if self is None:
                return None
            return self
        
        def peripheralManagerDidUpdateState_(self, peripheral):
            """Called when the peripheral manager's state changes."""
            state = peripheral.state()
            
            state_names = {
                CBPeripheralManagerStatePoweredOn: "Powered On",
                CBPeripheralManagerStatePoweredOff: "Powered Off",
                CBPeripheralManagerStateUnauthorized: "Unauthorized",
                CBPeripheralManagerStateUnsupported: "Unsupported",
            }
            state_name = state_names.get(state, f"Unknown ({state})")
            
            if state == CBPeripheralManagerStatePoweredOn:
                logger.info("✅ Bluetooth is powered on")
                server._setup_service()
            else:
                logger.warning(f"⚠️ Bluetooth state: {state_name}")
        
        def peripheralManager_didAddService_error_(self, peripheral, service, error):
            """Called when a service is added."""
            if error:
                logger.error(f"❌ Failed to add service: {error}")
            else:
                logger.info("✅ Service added successfully")
                server._start_advertising()
        
        def peripheralManagerDidStartAdvertising_error_(self, peripheral, error):
            """Called when advertising starts."""
            if error:
                logger.error(f"❌ Failed to start advertising: {error}")
            else:
                logger.info("🦞 BlueLobster is now discoverable!")
        
        def peripheralManager_didReceiveWriteRequests_(self, peripheral, requests):
            """Called when a write request is received."""
            for request in requests:
                if request.characteristic().UUID().isEqual_(
                    CBUUID.UUIDWithString_(server.config.write_char_uuid)
                ):
                    data = bytes(request.value())
                    server._handle_write(data)
                    peripheral.respondToRequest_withResult_(request, 0)  # Success
    
    return PeripheralManagerDelegate.alloc().init()
