"""BiVital BLE CLI for scan/read/write/stream workflows."""

from __future__ import annotations

import argparse
import asyncio
import contextlib
import json
import os
import sys
import time
from dataclasses import asdict, is_dataclass
from uuid import UUID

from .ble import BiVitalBLEClient, resolve_uuid, select_decoded_value, split_target_selector
from .ble_characteristics import CHARACTERISTICS_BY_KEY


async def main_async(argv: list[str] | None = None) -> None:
    """Async BLE CLI entrypoint."""
    parser = _build_parser()
    args = parser.parse_args(argv)

    try:
        if args.command == "scan":
            await _cmd_scan(args)
            return
        if args.command == "known":
            _cmd_known()
            return
        if args.command in {"gatt", "read", "write", "stream"}:
            await _cmd_connected(args)
            return
        parser.print_help()
    except RuntimeError as exc:
        raise SystemExit(f"Error: {exc}") from exc


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="bvtool ble",
        description="BI-Vital Bluetooth Low Energy commands.",
    )
    sub = parser.add_subparsers(dest="command")

    p_scan = sub.add_parser("scan", help="Scan BI-Vital devices")
    p_scan.add_argument("--all", action="store_true", help="Show all BLE devices")
    p_scan.add_argument("--scan-timeout", type=float, default=5.0, help="Scan timeout in seconds")
    p_scan.add_argument("--scan-retries", type=int, default=3, help="Scan retries on BlueZ lock")

    sub.add_parser("known", help="List known BLE characteristic keys")

    p_gatt = sub.add_parser("gatt", help="List GATT services/characteristics")
    _add_connection_args(p_gatt)

    p_read = sub.add_parser("read", help="Read one value")
    _add_connection_args(p_read)
    p_read.add_argument("key", help="Characteristic key/UUID (selectors allowed, e.g. heart_rate.bpm)")

    p_write = sub.add_parser("write", help="Write one value")
    _add_connection_args(p_write)
    p_write.add_argument("key", help="Characteristic key/UUID (no selector)")
    p_write.add_argument("value", help="Value text (or hex:/utf8: for unknown UUIDs)")
    p_write.add_argument("--without-response", action="store_true", help="Use write without response")

    p_stream = sub.add_parser("stream", help="Stream one or more values")
    _add_connection_args(p_stream)
    p_stream.add_argument(
        "keys",
        nargs="+",
        help="One or more keys/UUIDs. Comma-separated tokens are also accepted.",
    )
    p_stream.add_argument(
        "--duration",
        type=_parse_duration,
        default=None,
        help="Duration in seconds. Use inf/-1 for infinite (default).",
    )

    return parser


def _add_connection_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--address",
        required=True,
        help="Device identifier: full MAC or short token from name/MAC suffix (e.g. 8F25).",
    )
    parser.add_argument(
        "--connect-timeout",
        type=float,
        default=15.0,
        help="Connection timeout per attempt in seconds",
    )
    parser.add_argument(
        "--retries",
        type=int,
        default=3,
        help="Connection attempts",
    )


async def _cmd_scan(args: argparse.Namespace) -> None:
    devices = await BiVitalBLEClient.scan(timeout=args.scan_timeout, retries=args.scan_retries)
    if not args.all:
        devices = [d for d in devices if (d.name or "").lower().startswith("bi-vital")]
    for d in devices:
        print(f"{d.address}\t{d.name or 'Unknown'}")


def _cmd_known() -> None:
    for key in sorted(CHARACTERISTICS_BY_KEY):
        item = CHARACTERISTICS_BY_KEY[key]
        readable = "R" if item.readable else "-"
        stream = "S" if item.stream else "-"
        writable = "W" if item.writable else "-"
        print(f"{key}\t{item.uuid}\t{readable}{stream}{writable}\t{item.name}")


async def _cmd_connected(args: argparse.Namespace) -> None:
    resolved = await BiVitalBLEClient.resolve_address(args.address)
    print(f"Resolved device '{args.address}' -> {resolved}")
    client = BiVitalBLEClient(address=resolved)
    await client.connect_with_retry(timeout=args.connect_timeout, retries=args.retries)
    try:
        print(f"Connected to {resolved}")
        if args.command == "gatt":
            gatt = await client.list_gatt()
            for service in gatt:
                print(f"\nSERVICE {service['service_uuid']} ({service['service_description']})")
                for char in service["characteristics"]:
                    props = ",".join(char["properties"])
                    print(f"  {char['uuid']}\t{props}\t{char['description']}")
            return

        if args.command == "read":
            _validate_target(args.key)
            base, selector = split_target_selector(args.key)
            raw = await client.read_characteristic(base)
            decoded = client.decode_payload(resolve_uuid(base), raw)
            value = select_decoded_value(decoded, selector)
            print(f"Read {args.key} ({resolve_uuid(base)})")
            print(f"raw_hex={raw.hex()}")
            print(f"decoded={value}")
            return

        if args.command == "write":
            _validate_target(args.key)
            base, selector = split_target_selector(args.key)
            if selector:
                raise SystemExit("Field selectors are not supported for write")
            await client.write_characteristic(base, args.value, response=not args.without_response)
            print(f"Wrote {args.key} ({resolve_uuid(base)}) value={args.value!r}")
            return

        if args.command == "stream":
            targets = _parse_stream_targets(args.keys)
            for target in targets:
                _validate_target(target)
            _print_stream_banner(targets, args.duration)
            await _run_streams(client, targets, args.duration)
            return
    finally:
        await client.disconnect()
        print("Disconnected")


def _validate_target(key_or_uuid: str) -> None:
    normalized, selector = split_target_selector(key_or_uuid)
    if normalized in CHARACTERISTICS_BY_KEY:
        if selector:
            return
        return
    try:
        UUID(resolve_uuid(key_or_uuid))
        return
    except Exception:  # noqa: BLE001
        pass
    known = ", ".join(sorted(CHARACTERISTICS_BY_KEY.keys()))
    raise SystemExit(
        f"Unknown characteristic key/uuid: '{key_or_uuid}'.\n"
        "Examples: heart_rate, motion_raw, motion_quaternion, motion_euler, steps.\n"
        f"Known keys: {known}"
    )


def _parse_stream_targets(stream_args: list[str] | None) -> list[str]:
    if not stream_args:
        return ["heart_rate"]
    out: list[str] = []
    for item in stream_args:
        out.extend([p.strip() for p in item.split(",") if p.strip()])
    return out or ["heart_rate"]


def _parse_duration(value: str) -> float | None:
    text = value.strip().lower()
    if text in {"inf", "infinity", "-1"}:
        return None
    parsed = float(text)
    if parsed <= 0:
        return None
    return parsed


def _print_stream_banner(targets: list[str], duration: float | None) -> None:
    joined = ", ".join(targets)
    if duration is None:
        print(f"Streaming {joined} until stop ...")
    else:
        print(f"Streaming {joined} for {duration}s ...")
    print("Press Ctrl+C, q, or Esc to stop.")


async def _run_streams(
    client: BiVitalBLEClient,
    targets: list[str],
    duration: float | None,
) -> None:
    stream_tasks: list[asyncio.Task] = []
    label_width = max(len(t) for t in targets) if targets else 8

    for target in targets:

        def _on_sample(sample, *, _target=target):
            if is_dataclass(sample):
                value = asdict(sample)
            else:
                value = sample
            ts = time.strftime("%H:%M:%S")
            text = _format_sample(value)
            if len(targets) == 1:
                print(f"{ts} | {text}")
            else:
                print(f"{ts} | {_target:<{label_width}} | {text}")

        stream_tasks.append(
            asyncio.create_task(
                client.stream_characteristic(target, on_data=_on_sample, duration_seconds=None)
            )
        )

    key_task: asyncio.Task | None = None
    if sys.stdin.isatty():
        key_task = asyncio.create_task(_wait_for_stop_key())

    timer_task: asyncio.Task | None = None
    if duration is not None:
        timer_task = asyncio.create_task(asyncio.sleep(duration))

    wait_set = set(stream_tasks)
    if key_task:
        wait_set.add(key_task)
    if timer_task:
        wait_set.add(timer_task)

    try:
        done, _ = await asyncio.wait(wait_set, return_when=asyncio.FIRST_COMPLETED)
        if key_task and key_task in done:
            print(f"Stop requested by key: {repr(key_task.result())}")
        for task in stream_tasks:
            if task in done and task.exception() is not None:
                raise task.exception()
    except KeyboardInterrupt:
        print("Stop requested by Ctrl+C")
    finally:
        for task in stream_tasks:
            task.cancel()
        if key_task:
            key_task.cancel()
        if timer_task:
            timer_task.cancel()
        for task in stream_tasks:
            with contextlib.suppress(asyncio.CancelledError):
                await task
        if key_task:
            with contextlib.suppress(asyncio.CancelledError):
                await key_task
        if timer_task:
            with contextlib.suppress(asyncio.CancelledError):
                await timer_task


async def _wait_for_stop_key() -> str:
    while True:
        key = await asyncio.to_thread(_read_one_key)
        if key in ("q", "Q", "\x1b"):
            return key


def _read_one_key() -> str:
    if os.name == "nt":
        return _read_one_key_windows()
    return _read_one_key_posix()


def _read_one_key_windows() -> str:
    import msvcrt

    while True:
        ch = msvcrt.getwch()
        if ch:
            return ch


def _read_one_key_posix() -> str:
    import termios

    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    new = termios.tcgetattr(fd)
    try:
        new[3] &= ~(termios.ICANON | termios.ECHO)
        new[6][termios.VMIN] = 1
        new[6][termios.VTIME] = 0
        termios.tcsetattr(fd, termios.TCSADRAIN, new)
        return sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)


def _format_sample(value: object) -> str:
    if isinstance(value, (dict, list, tuple)):
        return json.dumps(value, ensure_ascii=True, separators=(",", ":"))
    return str(value)


def main(argv: list[str] | None = None) -> None:
    """Synchronous BLE CLI entrypoint for integration in `bvtool`."""
    try:
        asyncio.run(main_async(argv))
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
