"""Android BLE transport backed by the Kotlin ``BleBridge`` singleton.

Calls into Java land through Chaquopy.  This module only loads on Android;
importing it on desktop raises ``RuntimeError`` from ``_bridge()``.

The public surface mirrors :class:`~ledgrab.core.devices.ble_transport.BLETransport`
so ``BLEClient`` can treat both backends identically.
"""

from __future__ import annotations

import asyncio
from typing import List

from ledgrab.core.devices.ble_transport import DiscoveredBLEDevice
from ledgrab.utils import get_logger
from ledgrab.utils.platform import is_android

logger = get_logger(__name__)


def _bridge():
    """Return the Kotlin ``BleBridge`` singleton, or raise on non-Android."""
    if not is_android():
        raise RuntimeError("AndroidBLETransport is only usable on Android")
    try:
        from java import jclass  # type: ignore[import-not-found]
    except ImportError as exc:
        raise RuntimeError("Chaquopy java interop not available") from exc
    return jclass("com.ledgrab.android.BleBridge").INSTANCE


async def android_ble_scan(timeout: float = 4.0) -> List[DiscoveredBLEDevice]:
    """Scan for BLE peripherals using the Android ``BleBridge``.

    Runs the blocking scan on a thread-pool thread so the asyncio event
    loop is not blocked during the scan window.
    """
    bridge = _bridge()
    timeout_ms = int(timeout * 1000)
    loop = asyncio.get_running_loop()

    def _scan() -> List[DiscoveredBLEDevice]:
        results = bridge.scan(timeout_ms)
        devices: List[DiscoveredBLEDevice] = []
        for entry in results:
            parts = str(entry).split("|", 2)
            if len(parts) < 3:
                continue
            address, name, rssi_str = parts
            try:
                rssi: int | None = int(rssi_str)
            except ValueError:
                rssi = None
            devices.append(DiscoveredBLEDevice(address=address, name=name or address, rssi=rssi))
        return devices

    devices = await loop.run_in_executor(None, _scan)
    devices.sort(key=lambda d: (d.rssi is None, -(d.rssi or 0)))
    return devices


class AndroidBLETransport:
    """BLE transport for Android — delegates to the Kotlin ``BleBridge`` singleton.

    Lifecycle is identical to :class:`~ledgrab.core.devices.ble_transport.BLETransport`:
        transport = AndroidBLETransport(address, write_char_uuid, ...)
        await transport.connect()
        await transport.write(b"...")
        await transport.close()
    """

    def __init__(
        self,
        address: str,
        write_char_uuid: str,
        write_with_response: bool = False,
        connect_timeout: float = 10.0,
    ) -> None:
        self._address = address
        self._write_char_uuid = write_char_uuid
        self._write_with_response = write_with_response
        self._handle: int | None = None
        self._lock = asyncio.Lock()

    @property
    def address(self) -> str:
        return self._address

    @property
    def is_connected(self) -> bool:
        return self._handle is not None and self._handle >= 0

    async def connect(self) -> None:
        if self.is_connected:
            return
        bridge = _bridge()
        loop = asyncio.get_running_loop()

        handle = await loop.run_in_executor(
            None, lambda: int(bridge.connect(self._address, self._write_char_uuid))
        )
        if handle < 0:
            raise RuntimeError(
                f"Failed to connect to BLE device {self._address} via Android bridge "
                f"(device not found, permission denied, or characteristic missing)"
            )
        self._handle = handle
        logger.info("Android BLE connected: address=%s handle=%d", self._address, handle)

    async def close(self) -> None:
        if self._handle is None:
            return
        bridge = _bridge()
        handle = self._handle
        self._handle = None
        loop = asyncio.get_running_loop()
        try:
            await loop.run_in_executor(None, lambda: bridge.disconnect(handle))
        except Exception as exc:
            logger.warning("Android BLE disconnect of %s raised: %s", self._address, exc)

    async def write(self, data: bytes, char_uuid: str | None = None) -> None:
        """Write bytes to a characteristic on the connected peripheral.

        Serialised through an internal lock — BLE stacks do not tolerate
        overlapping writes on the same characteristic.

        Args:
            data: payload bytes.
            char_uuid: currently unused on Android — the Kotlin BleBridge
                binds a single "write characteristic" at connect time. If a
                protocol's ``init_writes`` target a different characteristic
                (e.g. SP110E's ``ffe2``), we log a warning and route the
                write to the bound char anyway. This is lossy but keeps the
                protocol layer compatible across backends until the bridge
                grows per-write characteristic selection.
        """
        if not self.is_connected or self._handle is None:
            raise RuntimeError(f"Android BLE transport {self._address} not connected")
        if char_uuid and char_uuid.lower() != self._write_char_uuid.lower():
            logger.warning(
                "Android BLE transport: init write to %s ignored (bridge is "
                "bound to %s); vendor handshake may not complete",
                char_uuid,
                self._write_char_uuid,
            )
        bridge = _bridge()
        handle = self._handle
        with_response = self._write_with_response
        loop = asyncio.get_running_loop()

        async with self._lock:
            success = await loop.run_in_executor(
                None, lambda: bool(bridge.write(handle, data, with_response))
            )
        if not success:
            raise RuntimeError(f"Android BLE write to {self._address} failed")
