from __future__ import annotations

import os
import shutil
import subprocess
from typing import Callable

from autosana_cli import runtime, tunnel
from autosana_cli import devices as device_service
from autosana_cli.models import (
    DEFAULT_APPIUM_VERSION,
    DEFAULT_NODE_VERSION,
    DoctorCheck,
    DoctorReport,
    Platform,
)


def _has_command(command: str) -> bool:
    return shutil.which(command) is not None


# ---------------------------------------------------------------------------
# Checks — each returns DoctorCheck(s) with fixable metadata
# ---------------------------------------------------------------------------


def _check_node() -> DoctorCheck:
    node_bin = runtime.find_node()
    if node_bin is None:
        return DoctorCheck(
            "Node.js",
            False,
            f"v{DEFAULT_NODE_VERSION} not found",
            fixable=True,
            fix_label=f"Install Node.js v{DEFAULT_NODE_VERSION}",
            group="Core",
        )
    version = runtime.check_node_version(node_bin) or "unknown"
    return DoctorCheck("Node.js", True, f"v{version}", group="Core")


def _check_appium() -> list[DoctorCheck]:
    appium_bin = runtime.find_appium()
    if appium_bin is None:
        return [
            DoctorCheck(
                "Appium",
                False,
                f"v{DEFAULT_APPIUM_VERSION} not found",
                fixable=True,
                fix_label=f"Install Appium {DEFAULT_APPIUM_VERSION}",
                group="Core",
            )
        ]
    version = runtime.check_appium_version(appium_bin, env=runtime.runtime_env()) or "unknown"
    return [DoctorCheck("Appium", True, f"v{version}", group="Core")]


def _check_driver(platform: Platform) -> DoctorCheck:
    driver_name = "xcuitest" if platform is Platform.IOS else "uiautomator2"
    group = "iOS" if platform is Platform.IOS else "Android"
    if runtime.check_driver_installed(driver_name):
        return DoctorCheck(f"{driver_name} driver", True, "installed", group=group)
    return DoctorCheck(
        f"{driver_name} driver",
        False,
        "not found",
        fixable=True,
        fix_label=f"Install Appium {driver_name} driver",
        group=group,
    )


def _check_cloudflared() -> DoctorCheck:
    if tunnel.find_cloudflared_binary() is not None:
        return DoctorCheck("Cloudflared", True, "available", group="Core")
    try:
        tunnel.cloudflared_download_info()
    except RuntimeError as exc:
        return DoctorCheck("Cloudflared", False, str(exc), group="Core")
    return DoctorCheck("Cloudflared", True, "will download on first use", fixable=True, group="Core")


# ---------------------------------------------------------------------------
# Environment checks (catch silent failures)
# ---------------------------------------------------------------------------


def _check_disk_space() -> DoctorCheck:
    """Appium + WDA + simulators need several GB free."""
    try:
        st = os.statvfs(os.path.expanduser("~"))
        free_gb = (st.f_bavail * st.f_frsize) / (1024**3)
        if free_gb < 2:
            return DoctorCheck(
                "Disk Space", False,
                f"Only {free_gb:.1f} GB free — Appium and WDA need at least 2 GB",
                group="Core",
            )
        return DoctorCheck("Disk Space", True, f"{free_gb:.0f} GB free", group="Core")
    except Exception:
        return DoctorCheck("Disk Space", True, "Could not check (non-fatal)", group="Core")


def _check_xcode_license() -> DoctorCheck:
    """Xcode license must be accepted or xcodebuild/git fail with exit code 69."""
    try:
        result = subprocess.run(
            ["xcodebuild", "-checkFirstLaunchStatus"],
            capture_output=True, text=True, timeout=10,
        )
        if result.returncode == 0:
            return DoctorCheck("Xcode License", True, "accepted", group="iOS")
        return DoctorCheck(
            "Xcode License", False,
            "Not accepted — run: sudo xcodebuild -license accept",
            group="iOS",
        )
    except FileNotFoundError:
        return DoctorCheck("Xcode License", False, "xcodebuild not found", group="iOS")
    except Exception:
        return DoctorCheck("Xcode License", True, "Could not check (non-fatal)", group="iOS")


def _check_xcode_select() -> DoctorCheck:
    """Verify xcode-select points to a valid Xcode installation."""
    try:
        result = subprocess.run(
            ["xcode-select", "-p"],
            capture_output=True, text=True, timeout=10,
        )
        path = result.stdout.strip()
        if result.returncode != 0 or not path:
            return DoctorCheck(
                "Xcode Path", False,
                "xcode-select not configured — run: xcode-select --install",
                group="iOS",
            )
        return DoctorCheck("Xcode Path", True, path, group="iOS")
    except Exception:
        return DoctorCheck("Xcode Path", True, "Could not check (non-fatal)", group="iOS")


def _check_port_available(port: int = 4723) -> DoctorCheck:
    """Check if the default Appium port is free."""
    import socket

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(1)
            result = s.connect_ex(("127.0.0.1", port))
            if result == 0:
                return DoctorCheck(
                    "Port 4723", False,
                    f"Port {port} is in use — another Appium or server may be running",
                    group="Core",
                )
        return DoctorCheck("Port 4723", True, "available", group="Core")
    except Exception:
        return DoctorCheck("Port 4723", True, "Could not check (non-fatal)", group="Core")


# ---------------------------------------------------------------------------
# Platform-specific checks (manual-only)
# ---------------------------------------------------------------------------


def _check_ios() -> list[DoctorCheck]:
    checks: list[DoctorCheck] = []
    xcrun_ok = _has_command("xcrun")
    checks.append(
        DoctorCheck(
            "Xcode CLI Tools",
            xcrun_ok,
            "available" if xcrun_ok else "Run: xcode-select --install",
            group="iOS",
        )
    )
    if checks[-1].ok:
        try:
            devices = device_service.list_available_ios_simulators()
        except Exception as exc:
            checks.append(DoctorCheck("Simulators", False, f"Could not list: {exc}", group="iOS"))
        else:
            if devices:
                checks.append(DoctorCheck("Simulators", True, f"{len(devices)} available", group="iOS"))
            else:
                checks.append(
                    DoctorCheck(
                        "Simulators",
                        False,
                        "None available — Xcode > Settings > Platforms to download",
                        group="iOS",
                    )
                )
    return checks


def _check_sdk_runtime_match() -> DoctorCheck:
    """Check that at least one simulator runtime matches the Xcode SDK version (major.minor)."""
    try:
        result = subprocess.run(
            ["xcrun", "--sdk", "iphonesimulator", "--show-sdk-version"],
            capture_output=True, text=True, timeout=10,
        )
        sdk_version = result.stdout.strip()
        if not sdk_version:
            return DoctorCheck("SDK/Runtime", True, "Could not detect SDK version", group="iOS")

        sdk_parts = sdk_version.split(".")
        sdk_major = int(sdk_parts[0])
        sdk_minor = int(sdk_parts[1]) if len(sdk_parts) > 1 else 0

        devices = device_service.list_available_ios_simulators()
        # runtime looks like "com.apple.CoreSimulator.SimRuntime.iOS-18-1"
        runtime_versions: set[tuple[int, int]] = set()
        for d in devices:
            if d.runtime and "iOS" in d.runtime:
                parts = d.runtime.split("iOS-")[-1].split("-")
                try:
                    runtime_versions.add((int(parts[0]), int(parts[1]) if len(parts) > 1 else 0))
                except (ValueError, IndexError):
                    continue

        if not runtime_versions:
            return DoctorCheck("SDK/Runtime", False, "No iOS simulator runtimes found", group="iOS")

        # SDK 18.2 works with runtime 18.2, 18.3, etc. (same major, equal or higher minor)
        has_compatible = any(
            maj == sdk_major and mn >= sdk_minor for maj, mn in runtime_versions
        )
        if has_compatible:
            return DoctorCheck("SDK/Runtime", True, f"Xcode SDK {sdk_version} matches runtimes", group="iOS")

        available = ", ".join(f"{maj}.{mn}" for maj, mn in sorted(runtime_versions))
        return DoctorCheck(
            "SDK/Runtime",
            False,
            f"Xcode SDK is iOS {sdk_version} but runtimes are {available}. "
            f"Run: xcodebuild -downloadPlatform iOS",
            group="iOS",
        )
    except Exception:
        return DoctorCheck("SDK/Runtime", True, "Could not verify (non-fatal)", group="iOS")


def _check_java_home() -> DoctorCheck:
    java_home = os.environ.get("JAVA_HOME")
    if java_home and shutil.which("java"):
        return DoctorCheck("JAVA_HOME", True, java_home, group="Android")
    if java_home:
        return DoctorCheck("JAVA_HOME", False, "set but java binary not on PATH", group="Android")
    return DoctorCheck("JAVA_HOME", False, "not set — install a JDK and set JAVA_HOME", group="Android")


def _check_android_home() -> DoctorCheck:
    android_home = os.environ.get("ANDROID_HOME") or os.environ.get("ANDROID_SDK_ROOT")
    if android_home:
        return DoctorCheck("ANDROID_HOME", True, android_home, group="Android")
    return DoctorCheck(
        "ANDROID_HOME",
        False,
        "not set — install Android Studio and set ANDROID_HOME",
        group="Android",
    )


def _check_android() -> list[DoctorCheck]:
    checks: list[DoctorCheck] = []
    adb_ok = _has_command("adb")
    emulator_ok = _has_command("emulator")
    checks.append(
        DoctorCheck(
            "adb",
            adb_ok,
            "installed" if adb_ok else "SDK Manager > SDK Tools > Platform-Tools",
            group="Android",
        )
    )
    checks.append(
        DoctorCheck(
            "emulator",
            emulator_ok,
            "installed" if emulator_ok else "SDK Manager > SDK Tools > Android Emulator",
            group="Android",
        )
    )
    if adb_ok and emulator_ok:
        try:
            connected = device_service.list_connected_android_devices()
            avds = device_service.list_android_avds()
        except Exception as exc:
            checks.append(DoctorCheck("Devices", False, f"Could not inspect: {exc}", group="Android"))
        else:
            if connected:
                checks.append(DoctorCheck("Devices", True, f"{len(connected)} connected", group="Android"))
            elif avds:
                checks.append(DoctorCheck("Devices", True, f"{len(avds)} AVD(s) available", group="Android"))
            else:
                checks.append(
                    DoctorCheck(
                        "Devices",
                        False,
                        "None found — Android Studio > Device Manager",
                        group="Android",
                    )
                )
    return checks


# ---------------------------------------------------------------------------
# Preflight (check only)
# ---------------------------------------------------------------------------


def run_preflight(platform: Platform | None = None) -> DoctorReport:
    checks: list[DoctorCheck] = []

    # Core environment checks
    checks.append(_check_disk_space())
    checks.append(_check_port_available())
    checks.append(_check_node())
    checks.extend(_check_appium())
    checks.append(_check_cloudflared())

    if platform is Platform.IOS:
        checks.append(_check_driver(Platform.IOS))
        checks.append(_check_xcode_license())
        checks.append(_check_xcode_select())
        checks.extend(_check_ios())
        checks.append(_check_sdk_runtime_match())
        return DoctorReport(platform=platform, checks=checks)

    if platform is Platform.ANDROID:
        checks.append(_check_driver(Platform.ANDROID))
        checks.append(_check_java_home())
        checks.append(_check_android_home())
        checks.extend(_check_android())
        return DoctorReport(platform=platform, checks=checks)

    # Both platforms
    checks.append(_check_driver(Platform.IOS))
    checks.append(_check_xcode_license())
    checks.append(_check_xcode_select())
    checks.extend(_check_ios())
    checks.append(_check_sdk_runtime_match())
    checks.append(_check_driver(Platform.ANDROID))
    checks.append(_check_java_home())
    checks.append(_check_android_home())
    checks.extend(_check_android())
    return DoctorReport(platform=None, checks=checks)


# ---------------------------------------------------------------------------
# Fix (install fixable dependencies, then re-check)
# ---------------------------------------------------------------------------


def run_fix(
    platform: Platform | None = None,
    on_step: Callable[[str, str], None] | None = None,
) -> DoctorReport:
    def emit(name: str, status: str) -> None:
        if on_step:
            on_step(name, status)

    try:
        emit("node", "start")
        runtime.ensure_node()
        emit("node", "done")
    except Exception as exc:
        emit("node", f"error:{exc}")

    try:
        emit("appium", "start")
        runtime.ensure_appium()
        emit("appium", "done")
    except Exception as exc:
        emit("appium", f"error:{exc}")

    try:
        emit("drivers", "start")
        runtime.ensure_drivers(platform)
        emit("drivers", "done")
    except Exception as exc:
        emit("drivers", f"error:{exc}")

    try:
        emit("cloudflared", "start")
        tunnel.ensure_cloudflared()
        emit("cloudflared", "done")
    except Exception as exc:
        emit("cloudflared", f"error:{exc}")

    return run_preflight(platform)
