import dataclasses
import logging
import sys
import tempfile
from contextlib import nullcontext
from functools import partial
from pathlib import Path
from typing import Annotated, Optional, TextIO

import typer
from typer_injector import InjectingTyper

from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing_manual_pairing
from pymobiledevice3.cli.cli_common import (
    RSDServiceProviderDep,
    async_command,
    print_json,
    prompt_device_list,
    sudo_required,
    user_requested_colored_output,
)
from pymobiledevice3.common import get_home_folder
from pymobiledevice3.exceptions import NoDeviceConnectedError
from pymobiledevice3.pair_records import PAIRING_RECORD_EXT, get_remote_pairing_record_filename
from pymobiledevice3.remote.common import ConnectionType, TunnelProtocol
from pymobiledevice3.remote.module_imports import MAX_IDLE_TIMEOUT, start_tunnel, verify_tunnel_imports
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT
from pymobiledevice3.remote.tunnel_service import (
    RemotePairingManualPairingService,
    get_core_device_tunnel_services,
    get_remote_pairing_tunnel_services,
)
from pymobiledevice3.remote.utils import get_rsds
from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS
from pymobiledevice3.tunneld.server import TunneldRunner
from pymobiledevice3.utils import run_in_loop

logger = logging.getLogger(__name__)


async def browse_rsd(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[dict]:
    devices = []
    for rsd in await get_rsds(timeout):
        assert rsd.peer_info is not None
        devices.append({
            "address": rsd.service.address[0],
            "port": RSD_PORT,
            "UniqueDeviceID": rsd.peer_info["Properties"]["UniqueDeviceID"],
            "ProductType": rsd.peer_info["Properties"]["ProductType"],
            "OSVersion": rsd.peer_info["Properties"]["OSVersion"],
        })
    return devices


async def browse_remotepairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[dict]:
    devices = []
    for remotepairing in await get_remote_pairing_tunnel_services(timeout):
        devices.append({
            "address": remotepairing.hostname,
            "port": remotepairing.port,
            "identifier": remotepairing.remote_identifier,
        })
    return devices


async def cli_browse(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> None:
    print_json({
        "usb": await browse_rsd(timeout),
        "wifi": await browse_remotepairing(timeout),
    })


cli = InjectingTyper(
    name="remote",
    help="Create and browse RemoteXPC tunnels (RSD/tunneld) for developer services.",
    no_args_is_help=True,
)


@cli.command("tunneld")
@sudo_required
def cli_tunneld(
    host: Annotated[str, typer.Option(help="Address to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[0],
    port: Annotated[int, typer.Option(help="Port to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[1],
    daemonize: Annotated[bool, typer.Option("--daemonize", "-d", help="Run tunneld in the background.")] = False,
    protocol: Annotated[
        TunnelProtocol,
        typer.Option(
            "--protocol",
            "-p",
            case_sensitive=False,
            help="Transport protocol for tunneld (default: TCP on Python >=3.13, otherwise QUIC).",
        ),
    ] = TunnelProtocol.DEFAULT,
    usb: Annotated[bool, typer.Option(help="Enable USB monitoring")] = True,
    wifi: Annotated[bool, typer.Option(help="Enable WiFi monitoring")] = True,
    usbmux: Annotated[bool, typer.Option(help="Enable usbmux monitoring")] = True,
    mobdev2: Annotated[bool, typer.Option(help="Enable mobdev2 monitoring")] = True,
) -> None:
    """Start Tunneld service for remote tunneling"""
    if not verify_tunnel_imports():
        return
    tunneld_runner = partial(
        TunneldRunner.create,
        host,
        port,
        protocol=protocol,
        usb_monitor=usb,
        wifi_monitor=wifi,
        usbmux_monitor=usbmux,
        mobdev2_monitor=mobdev2,
    )
    if daemonize:
        try:
            from daemonize import Daemonize
        except ImportError as e:
            raise NotImplementedError("daemonizing is only supported on unix platforms") from e
        with tempfile.NamedTemporaryFile("wt") as pid_file:
            daemon = Daemonize(app=f"Tunneld {host}:{port}", pid=pid_file.name, action=tunneld_runner)
            logger.info(f"starting Tunneld {host}:{port}")
            daemon.start()
    else:
        tunneld_runner()


@cli.command("browse")
@async_command
async def browse(
    timeout: Annotated[float, typer.Option(help="Bonjour timeout (in seconds)")] = DEFAULT_BONJOUR_TIMEOUT,
) -> None:
    """browse RemoteXPC devices using bonjour"""
    await cli_browse(timeout)


@cli.command("rsd-info")
def rsd_info(service_provider: RSDServiceProviderDep) -> None:
    """show info extracted from RSD peer"""
    print_json(service_provider.peer_info)


async def tunnel_task(
    service,
    secrets: Optional[TextIO] = None,
    script_mode: bool = False,
    max_idle_timeout: float = MAX_IDLE_TIMEOUT,
    protocol: TunnelProtocol = TunnelProtocol.DEFAULT,
) -> None:
    async with start_tunnel(
        service, secrets=secrets, max_idle_timeout=max_idle_timeout, protocol=protocol
    ) as tunnel_result:
        logger.info("tunnel created")
        if script_mode:
            print(f"{tunnel_result.address} {tunnel_result.port}", flush=True)
        else:
            if user_requested_colored_output():
                if secrets is not None:
                    print(
                        typer.style("Secrets: ", bold=True, fg="magenta")
                        + typer.style(secrets.name, bold=True, fg="white")
                    )
                print(
                    typer.style("Identifier: ", bold=True, fg="yellow")
                    + typer.style(service.remote_identifier, bold=True, fg="white")
                )
                print(
                    typer.style("Interface: ", bold=True, fg="yellow")
                    + typer.style(tunnel_result.interface, bold=True, fg="white")
                )
                print(
                    typer.style("Protocol: ", bold=True, fg="yellow")
                    + typer.style(tunnel_result.protocol, bold=True, fg="white")
                )
                print(
                    typer.style("RSD Address: ", bold=True, fg="yellow")
                    + typer.style(tunnel_result.address, bold=True, fg="white")
                )
                print(
                    typer.style("RSD Port: ", bold=True, fg="yellow")
                    + typer.style(tunnel_result.port, bold=True, fg="white")
                )
                print(
                    typer.style("Use the follow connection option:\n", bold=True, fg="yellow")
                    + typer.style(f"--rsd {tunnel_result.address} {tunnel_result.port}", bold=True, fg="cyan")
                )
            else:
                if secrets is not None:
                    print(f"Secrets: {secrets.name}")
                print(f"Identifier: {service.remote_identifier}")
                print(f"Interface: {tunnel_result.interface}")
                print(f"Protocol: {tunnel_result.protocol}")
                print(f"RSD Address: {tunnel_result.address}")
                print(f"RSD Port: {tunnel_result.port}")
                print(f"Use the follow connection option:\n--rsd {tunnel_result.address} {tunnel_result.port}")
        sys.stdout.flush()
        await tunnel_result.client.wait_closed()
        logger.info("tunnel was closed")


async def start_tunnel_task(
    connection_type: ConnectionType,
    secrets: Optional[TextIO],
    udid: Optional[str] = None,
    script_mode: bool = False,
    max_idle_timeout: float = MAX_IDLE_TIMEOUT,
    protocol: TunnelProtocol = TunnelProtocol.DEFAULT,
) -> None:
    if start_tunnel is None:
        raise NotImplementedError("failed to start the tunnel on your platform")
    get_tunnel_services = {
        connection_type.USB: get_core_device_tunnel_services,
        connection_type.WIFI: get_remote_pairing_tunnel_services,
    }
    tunnel_services = await get_tunnel_services[connection_type](udid=udid)
    if not tunnel_services:
        # no devices were found
        raise NoDeviceConnectedError()
    if len(tunnel_services) == 1 or udid is not None:
        # only one device found
        service = tunnel_services[0]
    else:
        # several devices were found, show prompt if none explicitly selected
        service = prompt_device_list(tunnel_services)

    await tunnel_task(
        service, secrets=secrets, script_mode=script_mode, max_idle_timeout=max_idle_timeout, protocol=protocol
    )


@cli.command("start-tunnel")
@sudo_required
@async_command
async def cli_start_tunnel(
    connection_type: Annotated[
        ConnectionType,
        typer.Option(
            "--connection-type",
            "-t",
            case_sensitive=False,
            help="Connection interface to tunnel (USB, WiFi, etc.).",
        ),
    ] = ConnectionType.USB,
    udid: Annotated[
        Optional[str],
        typer.Option(help="UDID for a specific device to look for"),
    ] = None,
    secrets: Annotated[
        Optional[Path],
        typer.Option(help="File to write TLS secrets for Wireshark decryption."),
    ] = None,
    script_mode: Annotated[
        bool,
        typer.Option(help="Print only HOST and port for scripts instead of formatted output."),
    ] = False,
    max_idle_timeout: Annotated[
        float,
        typer.Option(help="Maximum idle time before QUIC keepalive pings are sent."),
    ] = MAX_IDLE_TIMEOUT,
    protocol: Annotated[
        TunnelProtocol,
        typer.Option(
            "--protocol",
            "-p",
            case_sensitive=False,
            help="Transport protocol for the tunnel (default: TCP on Python >=3.13, otherwise QUIC).",
        ),
    ] = TunnelProtocol.DEFAULT,
) -> None:
    """start tunnel"""
    if not verify_tunnel_imports():
        return
    with secrets.open("wt") if secrets is not None else nullcontext() as secrets_file:
        await start_tunnel_task(
            connection_type,
            secrets_file,
            udid,
            script_mode,
            max_idle_timeout=max_idle_timeout,
            protocol=protocol,
        )


@dataclasses.dataclass
class RemotePairingManualPairingDevice:
    ip: str
    port: int
    device_name: str
    identifier: str


async def start_remote_pair_task(device_name: Optional[str]) -> None:
    if start_tunnel is None:
        raise NotImplementedError("failed to start the tunnel on your platform")

    devices: list[RemotePairingManualPairingDevice] = []
    for answer in await browse_remotepairing_manual_pairing():
        current_device_name = answer.properties["name"]

        if device_name is not None and current_device_name != device_name:
            continue

        for address in answer.addresses:
            devices.append(
                RemotePairingManualPairingDevice(
                    ip=address.full_ip,
                    port=answer.port,
                    device_name=current_device_name,
                    identifier=answer.properties["identifier"],
                )
            )

    if len(devices) > 0:
        device = prompt_device_list(devices)
    else:
        logger.error("No devices were found during bonjour browse")
        return

    async with RemotePairingManualPairingService(device.identifier, device.ip, device.port) as service:
        await service.connect(autopair=True)


@cli.command("pair")
@async_command
async def cli_pair(
    name: Annotated[
        Optional[str],
        typer.Option(help="Device name for a specific device to look for"),
    ] = None,
) -> None:
    """start remote pairing for devices which allow"""
    await start_remote_pair_task(name)


@cli.command("delete-pair")
def cli_delete_pair(udid: str) -> None:
    """Delete a pairing record"""
    pair_record_path = get_home_folder() / f"{get_remote_pairing_record_filename(udid)}.{PAIRING_RECORD_EXT}"
    pair_record_path.unlink()


@cli.command("service")
def cli_service(service_provider: RSDServiceProviderDep, service_name: str) -> None:
    """Start an ipython shell for interacting with given service"""
    service = service_provider.start_remote_service(service_name)
    run_in_loop(service.connect())
    service.shell()
