import asyncio
import threading
from typing import List, Optional

import cmd2
from cmd2 import Cmd2ArgumentParser, with_argparser, with_category

from androidtvremote2 import (
    AndroidTVRemote,
    CannotConnect,
    ConnectionClosed,
    InvalidAuth,
)

from gtv.constants import (
    APP_SHORTCUTS,
    BANNER,
    CAT_APPS,
    CAT_CONNECTION,
    CAT_DEVICE,
    CAT_MEDIA,
    CAT_NAVIGATION,
    CAT_NUMBERS,
    CAT_VOLUME,
    CERT_FILE,
    CLIENT_NAME,
    KEY_FILE,
    RAW_KEY_CODES,
    REMOTE_KEY_COMMANDS,
)
from gtv.discovery import discover_devices, discover_devices_bluetooth
from gtv.models import BtDeviceInfo, DeviceInfo
from gtv.remote import async_pair, ensure_cert_dir


class GtvApp(cmd2.Cmd):

    def __init__(self):
        super().__init__(
            allow_cli_args=False,
            auto_load_commands=False,
        )
        self.intro = BANNER + "\n  Type 'help' for commands. Use 'discover' to find devices.\n"
        self.prompt = "gtv> "
        self.self_in_py = True

        self.remote: Optional[AndroidTVRemote] = None
        self.connected_host: Optional[str] = None
        self.discovered_devices: List[DeviceInfo] = []
        self._loop: Optional[asyncio.AbstractEventLoop] = None
        self._loop_thread: Optional[threading.Thread] = None

        self.hidden_commands.append("alias")
        self.hidden_commands.append("macro")
        self.hidden_commands.append("run_pyscript")
        self.hidden_commands.append("run_script")
        self.hidden_commands.append("edit")
        self.hidden_commands.append("shell")
        self.hidden_commands.append("shortcuts")

        self._start_event_loop()

    def _start_event_loop(self):
        self._loop = asyncio.new_event_loop()
        self._loop_thread = threading.Thread(
            target=self._loop.run_forever, daemon=True, name="gtv-asyncio"
        )
        self._loop_thread.start()

    def _run(self, coro):
        if not self._loop or not self._loop.is_running():
            self.perror("Event loop is not running.")
            return None
        future = asyncio.run_coroutine_threadsafe(coro, self._loop)
        return future.result()

    def _stop_event_loop(self):
        if self._loop:
            self._loop.call_soon_threadsafe(self._loop.stop)
        if self._loop_thread:
            self._loop_thread.join(timeout=3)

    def _require_connection(self) -> bool:
        if not self.connected_host:
            self.perror("Not connected. Use 'connect' first.")
            return False
        if not self.remote:
            return self._auto_reconnect()
        return True

    def _auto_reconnect(self) -> bool:
        if not self.connected_host:
            return False
        host = self.connected_host
        self.poutput(f"Reconnecting to {host}...")

        if self.remote:
            try:
                self.remote.disconnect()
            except Exception:
                pass
            self.remote = None

        async def do_reconnect():
            remote = AndroidTVRemote(
                CLIENT_NAME, str(CERT_FILE), str(KEY_FILE), host,
                loop=self._loop,
            )
            try:
                await remote.async_connect()
                return remote
            except (CannotConnect, ConnectionClosed, InvalidAuth):
                return None

        remote = self._run(do_reconnect())
        if not remote:
            self.perror("Could not reconnect.")
            self.connected_host = None
            self._update_prompt()
            return False

        self.remote = remote
        remote.add_is_on_updated_callback(self._on_is_on)
        remote.add_current_app_updated_callback(self._on_current_app)
        remote.add_volume_info_updated_callback(self._on_volume_info)
        remote.add_is_available_updated_callback(self._on_available)
        remote.keep_reconnecting()
        self._update_prompt()
        self.poutput(f"Reconnected!")
        return True

    def _send_key(self, key_code: str):
        if not self._require_connection():
            return
        try:
            self.remote.send_key_command(key_code)
        except ConnectionClosed:
            if self._auto_reconnect():
                try:
                    self.remote.send_key_command(key_code)
                    return
                except Exception:
                    pass
            self.perror("Connection lost.")
        except ValueError as e:
            self.perror(f"Invalid key: {e}")

    def _format_volume(self, vol: Optional[dict]) -> str:
        if not vol:
            return "N/A"
        muted = " (muted)" if vol.get("muted") else ""
        return f"{vol.get('level', '?')}/{vol.get('max', '?')}{muted}"

    def _print_device_table(self, devices: List[DeviceInfo]):
        if not devices:
            self.poutput("No Android TV / Google TV devices found.")
            self.poutput("Make sure your TV is on and on the same network.")
            return
        self.poutput(f"\nFound {len(devices)} device(s):\n")
        self.poutput(f"  {'#':<4} {'Name':<30} {'IP Address':<18} {'Port':<6}")
        self.poutput(f"  {'─'*4} {'─'*30} {'─'*18} {'─'*6}")
        for i, dev in enumerate(devices, 1):
            self.poutput(f"  {i:<4} {dev.name:<30} {dev.host:<18} {dev.port:<6}")
        self.poutput("")

    def _print_bt_device_table(self, devices: List[BtDeviceInfo]):
        if not devices:
            self.poutput("No Bluetooth devices found.")
            self.poutput("Make sure your TV has Bluetooth enabled and is nearby.")
            return
        likely = [d for d in devices if d.is_likely_atv]
        self.poutput(f"\nFound {len(devices)} device(s)"
                     f" ({len(likely)} likely Android TV):\n")
        self.poutput(f"  {'#':<4} {'Name':<30} {'Address':<20} {'RSSI':<6} {'Type'}")
        self.poutput(f"  {'─'*4} {'─'*30} {'─'*20} {'─'*6} {'─'*12}")
        for i, dev in enumerate(devices, 1):
            tag = "Android TV?" if dev.is_likely_atv else ""
            self.poutput(f"  {i:<4} {dev.name:<30} {dev.address:<20} {dev.rssi:<6} {tag}")
        self.poutput("")
        self.poutput("  Note: To connect, use the device's IP address (use 'discover' without -b).")
        self.poutput("")

    def _update_prompt(self):
        if self.connected_host:
            is_on = self.remote.is_on if self.remote else False
            state = "on" if is_on else "off"
            self.prompt = f"gtv ({self.connected_host} [{state}])> "
        else:
            self.prompt = "gtv> "

    def _on_is_on(self, is_on: bool):
        state = "ON" if is_on else "OFF"
        self._update_prompt()
        self.async_alert(f"[Device power: {state}]")

    def _on_current_app(self, app: str):
        self.async_alert(f"[App: {app}]")

    def _on_volume_info(self, vol: dict):
        self.async_alert(f"[Volume: {self._format_volume(vol)}]")

    def _on_available(self, available: bool):
        if available:
            self.async_alert(f"[Reconnected]")
        else:
            self.async_alert(f"[Connection lost, will auto-reconnect]")
        self._update_prompt()

    discover_parser = Cmd2ArgumentParser(
        description="Scan for Android TV devices on the network.",
    )
    discover_parser.add_argument(
        "-t", "--timeout", type=float, default=5.0,
        help="scan timeout in seconds (default: 5 for mDNS, 8 for BLE)",
    )
    discover_parser.add_argument(
        "-b", "--bluetooth", action="store_true",
        help="scan via Bluetooth Low Energy instead of mDNS",
    )
    discover_parser.add_argument(
        "-a", "--all", action="store_true", dest="show_all",
        help="(with --bluetooth) show all BLE devices, not just likely Android TVs",
    )

    @with_argparser(discover_parser)
    @with_category(CAT_CONNECTION)
    def do_discover(self, args):
        if args.bluetooth:
            bt_timeout = args.timeout if args.timeout != 5.0 else 8.0
            self.poutput(f"\nScanning for Bluetooth devices ({bt_timeout}s)...")
            bt_devices = self._run(
                discover_devices_bluetooth(bt_timeout, show_all=args.show_all)
            )
            self._print_bt_device_table(bt_devices)
        else:
            self.poutput(f"\nScanning for Android TV devices via mDNS ({args.timeout}s)...")
            self.discovered_devices = self._run(discover_devices(args.timeout))
            self._print_device_table(self.discovered_devices)

    pair_parser = Cmd2ArgumentParser(description="Pair with an Android TV device.")
    pair_parser.add_argument("host", nargs="?", help="IP address (omit to discover)")
    pair_parser.add_argument(
        "-t", "--timeout", type=float, default=5.0, help="discovery timeout",
    )

    @with_argparser(pair_parser)
    @with_category(CAT_CONNECTION)
    def do_pair(self, args):
        host = args.host
        if not host:
            self.poutput(f"\nScanning for devices ({args.timeout}s)...")
            self.discovered_devices = self._run(discover_devices(args.timeout))
            self._print_device_table(self.discovered_devices)
            if not self.discovered_devices:
                return
            choice = input("  Enter device # or IP address: ").strip()
            host = self._resolve_device_choice(choice)
            if not host:
                return

        self._run(async_pair(host, self.poutput))

    connect_parser = Cmd2ArgumentParser(
        description="Connect to an Android TV and start remote control.",
    )
    connect_parser.add_argument("host", nargs="?", help="IP address (omit to discover)")
    connect_parser.add_argument(
        "-t", "--timeout", type=float, default=5.0, help="discovery timeout",
    )

    @with_argparser(connect_parser)
    @with_category(CAT_CONNECTION)
    def do_connect(self, args):
        host = args.host
        if not host:
            self.poutput(f"\nScanning for devices ({args.timeout}s)...")
            self.discovered_devices = self._run(discover_devices(args.timeout))
            self._print_device_table(self.discovered_devices)
            if not self.discovered_devices:
                return
            if len(self.discovered_devices) == 1:
                host = self.discovered_devices[0].host
                self.poutput(f"Auto-selecting: {self.discovered_devices[0]}")
            else:
                choice = input("  Enter device # or IP address: ").strip()
                host = self._resolve_device_choice(choice)
                if not host:
                    return

        self._connect_to(host)

    @with_category(CAT_CONNECTION)
    def do_disconnect(self, _):
        if not self._require_connection():
            return
        self.remote.disconnect()
        self.poutput(f"Disconnected from {self.connected_host}.")
        self.connected_host = None
        self.remote = None
        self._update_prompt()

    def _resolve_device_choice(self, choice: str) -> Optional[str]:
        try:
            idx = int(choice) - 1
            if 0 <= idx < len(self.discovered_devices):
                return self.discovered_devices[idx].host
            self.perror("Invalid device number.")
            return None
        except ValueError:
            return choice if choice else None

    def _connect_to(self, host: str):
        ensure_cert_dir()

        async def do_connect():
            remote = AndroidTVRemote(
                CLIENT_NAME, str(CERT_FILE), str(KEY_FILE), host,
                loop=self._loop,
            )

            if await remote.async_generate_cert_if_missing():
                self.poutput("Generated new certificate. Pairing required.")
                success = await async_pair(host, self.poutput)
                if not success:
                    return None
                remote = AndroidTVRemote(
                    CLIENT_NAME, str(CERT_FILE), str(KEY_FILE), host,
                    loop=self._loop,
                )

            try:
                await remote.async_connect()
            except InvalidAuth:
                self.poutput("Authentication failed. Pairing required.")
                success = await async_pair(host, self.poutput)
                if not success:
                    return None
                remote = AndroidTVRemote(
                    CLIENT_NAME, str(CERT_FILE), str(KEY_FILE), host,
                    loop=self._loop,
                )
                try:
                    await remote.async_connect()
                except (CannotConnect, ConnectionClosed) as e:
                    self.perror(f"Connection failed: {e}")
                    return None
            except (CannotConnect, ConnectionClosed) as e:
                self.perror(f"Cannot connect to {host}: {e}")
                return None

            return remote

        self.poutput(f"Connecting to {host}...")
        remote = self._run(do_connect())
        if not remote:
            return

        self.remote = remote
        self.connected_host = host

        remote.add_is_on_updated_callback(self._on_is_on)
        remote.add_current_app_updated_callback(self._on_current_app)
        remote.add_volume_info_updated_callback(self._on_volume_info)
        remote.add_is_available_updated_callback(self._on_available)
        remote.keep_reconnecting()

        self._update_prompt()
        self.poutput(f"Connected to {host}!")
        self._show_status()

    @with_category(CAT_NAVIGATION)
    def do_up(self, _):
        self._send_key("DPAD_UP")

    @with_category(CAT_NAVIGATION)
    def do_down(self, _):
        self._send_key("DPAD_DOWN")

    @with_category(CAT_NAVIGATION)
    def do_left(self, _):
        self._send_key("DPAD_LEFT")

    @with_category(CAT_NAVIGATION)
    def do_right(self, _):
        self._send_key("DPAD_RIGHT")

    @with_category(CAT_NAVIGATION)
    def do_enter(self, _):
        self._send_key("DPAD_CENTER")

    @with_category(CAT_NAVIGATION)
    def do_select(self, _):
        self._send_key("DPAD_CENTER")

    @with_category(CAT_NAVIGATION)
    def do_ok(self, _):
        self._send_key("DPAD_CENTER")

    @with_category(CAT_NAVIGATION)
    def do_back(self, _):
        self._send_key("BACK")

    @with_category(CAT_NAVIGATION)
    def do_home(self, _):
        self._send_key("HOME")

    @with_category(CAT_NAVIGATION)
    def do_menu(self, _):
        self._send_key("MENU")

    @with_category(CAT_NAVIGATION)
    def do_settings(self, _):
        self._send_key("SETTINGS")

    @with_category(CAT_NAVIGATION)
    def do_search(self, _):
        self._send_key("SEARCH")

    @with_category(CAT_NAVIGATION)
    def do_assist(self, _):
        self._send_key("ASSIST")

    @with_category(CAT_NAVIGATION)
    def do_guide(self, _):
        self._send_key("GUIDE")

    @with_category(CAT_NAVIGATION)
    def do_info(self, _):
        self._send_key("INFO")

    @with_category(CAT_NAVIGATION)
    def do_explorer(self, _):
        self._send_key("EXPLORER")

    @with_category(CAT_MEDIA)
    def do_play(self, _):
        self._send_key("MEDIA_PLAY")

    @with_category(CAT_MEDIA)
    def do_pause(self, _):
        self._send_key("MEDIA_PAUSE")

    @with_category(CAT_MEDIA)
    def do_playpause(self, _):
        self._send_key("MEDIA_PLAY_PAUSE")

    @with_category(CAT_MEDIA)
    def do_stop(self, _):
        self._send_key("MEDIA_STOP")

    @with_category(CAT_MEDIA)
    def do_next(self, _):
        self._send_key("MEDIA_NEXT")

    @with_category(CAT_MEDIA)
    def do_prev(self, _):
        self._send_key("MEDIA_PREVIOUS")

    @with_category(CAT_MEDIA)
    def do_previous(self, _):
        self._send_key("MEDIA_PREVIOUS")

    @with_category(CAT_MEDIA)
    def do_rewind(self, _):
        self._send_key("MEDIA_REWIND")

    @with_category(CAT_MEDIA)
    def do_ff(self, _):
        self._send_key("MEDIA_FAST_FORWARD")

    @with_category(CAT_MEDIA)
    def do_fastforward(self, _):
        self._send_key("MEDIA_FAST_FORWARD")

    @with_category(CAT_MEDIA)
    def do_record(self, _):
        self._send_key("MEDIA_RECORD")

    @with_category(CAT_MEDIA)
    def do_captions(self, _):
        self._send_key("CAPTIONS")

    @with_category(CAT_MEDIA)
    def do_audio(self, _):
        self._send_key("MEDIA_AUDIO_TRACK")

    @with_category(CAT_MEDIA)
    def do_chup(self, _):
        self._send_key("CHANNEL_UP")

    @with_category(CAT_MEDIA)
    def do_chdown(self, _):
        self._send_key("CHANNEL_DOWN")

    volup_parser = Cmd2ArgumentParser(description="Increase volume.")
    volup_parser.add_argument(
        "times", nargs="?", type=int, default=1,
        help="number of times to press (default: 1)",
    )

    @with_argparser(volup_parser)
    @with_category(CAT_VOLUME)
    def do_volup(self, args):
        for _ in range(args.times):
            self._send_key("VOLUME_UP")

    voldown_parser = Cmd2ArgumentParser(description="Decrease volume.")
    voldown_parser.add_argument(
        "times", nargs="?", type=int, default=1,
        help="number of times to press (default: 1)",
    )

    @with_argparser(voldown_parser)
    @with_category(CAT_VOLUME)
    def do_voldown(self, args):
        for _ in range(args.times):
            self._send_key("VOLUME_DOWN")

    @with_category(CAT_VOLUME)
    def do_mute(self, _):
        self._send_key("MUTE")

    @with_category(CAT_VOLUME)
    def do_power(self, _):
        self._send_key("POWER")

    app_parser = Cmd2ArgumentParser(description="Launch an app on the TV.")
    app_parser.add_argument(
        "name",
        help="app shortcut name or full package id (e.g. youtube, com.netflix.ninja)",
    )

    @with_argparser(app_parser)
    @with_category(CAT_APPS)
    def do_app(self, args):
        if not self._require_connection():
            return
        target = APP_SHORTCUTS.get(args.name.lower(), args.name)
        try:
            self.remote.send_launch_app_command(target)
            self.poutput(f"Launching: {target}")
        except ConnectionClosed:
            if self._auto_reconnect():
                try:
                    self.remote.send_launch_app_command(target)
                    self.poutput(f"Launching: {target}")
                    return
                except Exception:
                    pass
            self.perror("Connection lost.")

    def complete_app(self, text, line, begidx, endidx):
        return [name for name in sorted(APP_SHORTCUTS.keys()) if name.startswith(text.lower())]

    open_parser = Cmd2ArgumentParser(description="Open a URL or deep link on the TV.")
    open_parser.add_argument("url", help="URL or deep link to open")

    @with_argparser(open_parser)
    @with_category(CAT_APPS)
    def do_open(self, args):
        if not self._require_connection():
            return
        try:
            self.remote.send_launch_app_command(args.url)
            self.poutput(f"Opening: {args.url}")
        except ConnectionClosed:
            if self._auto_reconnect():
                try:
                    self.remote.send_launch_app_command(args.url)
                    self.poutput(f"Opening: {args.url}")
                    return
                except Exception:
                    pass
            self.perror("Connection lost.")

    key_parser = Cmd2ArgumentParser(description="Send a raw remote key code.")
    key_parser.add_argument("keycode", help="remote key code (e.g. DPAD_UP, POWER)")

    @with_argparser(key_parser)
    @with_category(CAT_APPS)
    def do_key(self, args):
        self._send_key(args.keycode.upper())

    def complete_key(self, text, line, begidx, endidx):
        return [k for k in RAW_KEY_CODES if k.startswith(text.upper())]

    for _digit in range(10):
        exec(f"""
@with_category(CAT_NUMBERS)
def do_{_digit}(self, _):
    self._send_key("{_digit}")
""")

    @with_category(CAT_NUMBERS)
    def do_red(self, _):
        self._send_key("PROG_RED")

    @with_category(CAT_NUMBERS)
    def do_green(self, _):
        self._send_key("PROG_GREEN")

    @with_category(CAT_NUMBERS)
    def do_yellow(self, _):
        self._send_key("PROG_YELLOW")

    @with_category(CAT_NUMBERS)
    def do_blue(self, _):
        self._send_key("PROG_BLUE")

    @with_category(CAT_NUMBERS)
    def do_teletext(self, _):
        self._send_key("TV_TELETEXT")

    @with_category(CAT_NUMBERS)
    def do_dvr(self, _):
        self._send_key("DVR")

    @with_category(CAT_DEVICE)
    def do_status(self, _):
        if not self._require_connection():
            return
        self._show_status()

    def _show_status(self):
        r = self.remote
        self.poutput(f"\n  Host:        {self.connected_host}")
        if r.device_info:
            for k, v in r.device_info.items():
                self.poutput(f"  {k:<12} {v}")
        self.poutput(f"  Power:       {'ON' if r.is_on else 'OFF'}")
        self.poutput(f"  App:         {r.current_app or 'N/A'}")
        self.poutput(f"  Volume:      {self._format_volume(r.volume_info)}")
        self.poutput("")

    @with_category(CAT_DEVICE)
    def do_apps(self, _):
        self.poutput("\nApp shortcuts (use with 'app <name>'):\n")
        self.poutput(f"  {'Shortcut':<15} {'Package / URL'}")
        self.poutput(f"  {'─'*15} {'─'*45}")
        for name, pkg in sorted(APP_SHORTCUTS.items()):
            self.poutput(f"  {name:<15} {pkg}")
        self.poutput("")

    @with_category(CAT_DEVICE)
    def do_keys(self, _):
        self.poutput("\nKey shortcuts (type directly as commands):\n")
        self.poutput(f"  {'Command':<15} {'Remote Key Code'}")
        self.poutput(f"  {'─'*15} {'─'*25}")
        seen = set()
        for alias, code in sorted(REMOTE_KEY_COMMANDS.items()):
            if alias not in seen:
                self.poutput(f"  {alias:<15} {code}")
                seen.add(alias)
        self.poutput(f"\nRaw key codes (use with 'key <CODE>'):")
        self.poutput(f"  {', '.join(RAW_KEY_CODES[:10])},")
        self.poutput(f"  {', '.join(RAW_KEY_CODES[10:20])},")
        self.poutput(f"  {', '.join(RAW_KEY_CODES[20:])}")
        self.poutput("")

    def do_quit(self, _):
        if self.remote:
            self.remote.disconnect()
            self.poutput(f"Disconnected from {self.connected_host}.")
        self._stop_event_loop()
        self.poutput("Goodbye!")
        return True

    do_exit = do_quit

    def postloop(self):
        if self.remote:
            try:
                self.remote.disconnect()
            except Exception:
                pass
        self._stop_event_loop()
