from __future__ import annotations

import json
import logging
import plistlib
import subprocess
import time

from autosana_cli.models import Device, InstalledApp, Platform
from autosana_cli.processes import detach_kwargs

logger = logging.getLogger(__name__)

# Bundle IDs to always exclude from app listings (Appium internals, test runners)
_SKIP_BUNDLE_IDS = frozenset(
    {
        "com.facebook.WebDriverAgentRunner.xctrunner",
        "com.facebook.WebDriverAgentRunner",
    }
)


def parse_ios_devices(payload: str) -> list[Device]:
    data = json.loads(payload)
    devices: list[Device] = []
    for runtime, runtime_devices in data.get("devices", {}).items():
        for device in runtime_devices:
            if not device.get("isAvailable", True):
                continue
            devices.append(
                Device(
                    platform=Platform.IOS,
                    name=str(device["name"]),
                    udid=str(device["udid"]),
                    booted=device.get("state") == "Booted",
                    runtime=runtime,
                )
            )
    return devices


def parse_adb_devices(payload: str) -> list[str]:
    serials: list[str] = []
    for line in payload.splitlines():
        parts = line.split()
        if len(parts) >= 2 and parts[1] == "device":
            serials.append(parts[0])
    return serials


def list_booted_ios_simulators() -> list[Device]:
    result = subprocess.run(
        ["xcrun", "simctl", "list", "devices", "booted", "-j"],
        check=True,
        capture_output=True,
        text=True,
    )
    return parse_ios_devices(result.stdout)


def list_available_ios_simulators() -> list[Device]:
    result = subprocess.run(
        ["xcrun", "simctl", "list", "devices", "-j"],
        check=True,
        capture_output=True,
        text=True,
    )
    return parse_ios_devices(result.stdout)


def runtime_sort_key(runtime: str | None) -> tuple[int, ...]:
    if not runtime:
        return (0,)
    numbers = []
    for chunk in runtime.replace("-", ".").split("."):
        if chunk.isdigit():
            numbers.append(int(chunk))
    return tuple(numbers or [0])


def choose_ios_device(devices: list[Device]) -> Device | None:
    booted = [device for device in devices if device.booted]
    if booted:
        return sorted(booted, key=lambda item: (runtime_sort_key(item.runtime), item.name), reverse=True)[0]
    iphones = [device for device in devices if "iphone" in device.name.lower()]
    if iphones:
        return sorted(iphones, key=lambda item: (runtime_sort_key(item.runtime), item.name), reverse=True)[0]
    if devices:
        return sorted(devices, key=lambda item: (runtime_sort_key(item.runtime), item.name), reverse=True)[0]
    return None


def boot_ios_simulator(device: Device) -> Device:
    subprocess.run(["xcrun", "simctl", "bootstatus", device.udid, "-b"], check=False, capture_output=True, text=True)
    subprocess.run(["xcrun", "simctl", "boot", device.udid], check=False, capture_output=True, text=True)
    subprocess.run(["xcrun", "simctl", "bootstatus", device.udid, "-b"], check=True, capture_output=True, text=True)
    return Device(
        platform=device.platform,
        name=device.name,
        udid=device.udid,
        booted=True,
        runtime=device.runtime,
    )


def list_installed_ios_apps(udid: str) -> list[InstalledApp]:
    """Best-effort: list user-installed apps on an iOS simulator.

    Uses ``xcrun simctl listapps`` (old-style plist) piped through
    ``plutil -convert xml1`` so that ``plistlib`` can parse it.
    Returns ``[]`` on any failure — caller checks the success flag.
    """
    raw = subprocess.run(
        ["xcrun", "simctl", "listapps", udid],
        capture_output=True,
    )
    if raw.returncode != 0:
        raise RuntimeError(f"xcrun simctl listapps failed: {raw.stderr.decode(errors='replace')[:200]}")

    # Convert old-style plist → XML plist so plistlib can read it
    converted = subprocess.run(
        ["plutil", "-convert", "xml1", "-o", "-", "--", "-"],
        input=raw.stdout,
        capture_output=True,
    )
    if converted.returncode != 0:
        raise RuntimeError("plutil conversion failed")

    data = plistlib.loads(converted.stdout)
    apps: list[InstalledApp] = []
    for bundle_id, info in data.items():
        if not isinstance(info, dict):
            continue
        if info.get("ApplicationType") != "User":
            continue
        if bundle_id in _SKIP_BUNDLE_IDS:
            continue
        if bundle_id.endswith(".xctrunner"):
            continue
        name = info.get("CFBundleDisplayName") or info.get("CFBundleName") or bundle_id
        apps.append(InstalledApp(bundle_id=str(bundle_id), name=str(name)))
    return apps


def list_installed_android_apps(serial: str) -> list[InstalledApp]:
    """Best-effort: list third-party apps on an Android device."""
    result = subprocess.run(
        ["adb", "-s", serial, "shell", "pm", "list", "packages", "-3"],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        raise RuntimeError(f"adb pm list packages failed: {result.stderr[:200]}")

    apps: list[InstalledApp] = []
    for line in result.stdout.splitlines():
        line = line.strip()
        if line.startswith("package:"):
            bundle_id = line[len("package:") :]
            if bundle_id:
                apps.append(InstalledApp(bundle_id=bundle_id, name=bundle_id))
    return apps


def list_installed_apps(device: Device) -> tuple[list[InstalledApp], bool]:
    """List installed apps on a device. Returns (apps, detection_succeeded).

    Never raises — returns ([], False) if detection fails so that
    ``autosana up`` is never blocked by app-detection issues.
    """
    try:
        if device.platform is Platform.IOS:
            apps = list_installed_ios_apps(device.udid)
        else:
            apps = list_installed_android_apps(device.udid)
        return apps, True
    except Exception as exc:
        logger.debug("App detection failed for %s: %s", device.udid, exc)
        return [], False


def list_booted_devices_with_apps(platform: Platform) -> list[dict]:
    """List all booted devices with their installed apps (for multi-device JSON output)."""
    if platform is Platform.IOS:
        booted = list_booted_ios_simulators()
    else:
        booted = list_connected_android_devices()

    result = []
    for d in booted:
        apps, ok = list_installed_apps(d)
        result.append(
            {
                "name": d.name,
                "udid": d.udid,
                "runtime": d.runtime,
                "apps": [a.to_dict() for a in apps],
                "app_detection": "ok" if ok else "failed",
            }
        )
    return result


def resolve_ios_device(auto_boot: bool = False, device_id: str | None = None) -> Device | None:
    """Find a booted iOS simulator, optionally filtered by name or UDID.

    Returns None instead of raising when no device is found and auto_boot is False.
    """
    booted = list_booted_ios_simulators()

    if device_id:
        # Match by UDID (exact) or name (case-insensitive prefix)
        for d in booted:
            if d.udid == device_id or d.name.lower() == device_id.lower():
                return d
        return None

    if booted:
        return choose_ios_device(booted) or booted[0]

    if not auto_boot:
        return None

    available = list_available_ios_simulators()
    device = choose_ios_device(available)
    if device is None:
        return None
    return boot_ios_simulator(device)


def list_connected_android_devices() -> list[Device]:
    result = subprocess.run(["adb", "devices"], check=True, capture_output=True, text=True)
    serials = parse_adb_devices(result.stdout)
    return [Device(platform=Platform.ANDROID, name=serial, udid=serial, booted=True) for serial in serials]


def list_android_avds() -> list[str]:
    result = subprocess.run(["emulator", "-list-avds"], check=True, capture_output=True, text=True)
    return [line.strip() for line in result.stdout.splitlines() if line.strip()]


def boot_android_emulator(avd_name: str, *, timeout: float = 60.0) -> Device:
    subprocess.Popen(
        ["emulator", "-avd", avd_name],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        **detach_kwargs(True),
    )
    deadline = time.time() + timeout
    while time.time() < deadline:
        devices = list_connected_android_devices()
        if devices:
            return devices[0]
        time.sleep(2)
    raise RuntimeError(f"Android emulator '{avd_name}' did not appear in adb within {int(timeout)}s.")


def resolve_android_device(auto_boot: bool = False, device_id: str | None = None) -> Device | None:
    """Find a connected Android device, optionally filtered by serial.

    Returns None instead of raising when no device is found and auto_boot is False.
    """
    connected = list_connected_android_devices()

    if device_id:
        for d in connected:
            if d.udid == device_id:
                return d
        return None

    if connected:
        return connected[0]

    if not auto_boot:
        return None

    avds = list_android_avds()
    if not avds:
        return None
    return boot_android_emulator(avds[0])


def resolve_device(platform: Platform, auto_boot: bool = False, device_id: str | None = None) -> Device | None:
    if platform is Platform.IOS:
        return resolve_ios_device(auto_boot=auto_boot, device_id=device_id)
    return resolve_android_device(auto_boot=auto_boot, device_id=device_id)
