"""
本地设备控制器 — 在 Termux 中运行时替代 uiautomator2

当 Agent 运行在手机本地（Termux）时，ADB 不可用。
此模块通过 ATX HTTP API + subprocess 直接控制设备，
提供与 uiautomator2 Device 兼容的接口。

ATX 服务端口：9008（由 u2 init 从电脑推送启动）
"""

import json
import logging
import subprocess
import time
from typing import Any, Dict, Optional
from urllib import request, error, parse

logger = logging.getLogger(__name__)

_ATX_PORTS = [9008, 7912]


class ShellResult:
    """兼容 u2 的 shell 返回值"""
    def __init__(self, output: str, exit_code: int = 0):
        self.output = output
        self.exit_code = exit_code


class _Settings(dict):
    """兼容 u2 的 settings 字典"""
    pass


class LocalDevice:
    """
    本地设备控制器 —— 兼容 uiautomator2.Device 接口的子集

    在手机本地运行时，shell 命令直接用 subprocess 执行，
    UI 操作通过 ATX HTTP server 完成。
    """

    def __init__(self, atx_url: Optional[str] = None):
        self._atx_url = atx_url or self._find_atx()
        self.settings = _Settings({
            'operation_delay': (0, 0),
            'operation_delay_methods': [],
        })
        self._implicit_wait = 10.0
        self._verify_atx()

    @staticmethod
    def _find_atx() -> str:
        for port in _ATX_PORTS:
            url = f"http://127.0.0.1:{port}"
            try:
                resp = request.urlopen(f"{url}/ping", timeout=2)
                if resp.status == 200:
                    return url
            except Exception:
                continue
        return f"http://127.0.0.1:{_ATX_PORTS[0]}"

    def _verify_atx(self):
        try:
            self._http_get("/info")
            logger.info(f"ATX 服务连接成功: {self._atx_url}")
        except Exception as e:
            logger.warning(f"ATX 服务不可用 ({self._atx_url}): {e}")
            raise ConnectionError(f"ATX server not reachable at {self._atx_url}")

    def _http_get(self, path: str, timeout: float = 10) -> Any:
        resp = request.urlopen(f"{self._atx_url}{path}", timeout=timeout)
        return json.loads(resp.read().decode())

    def _http_post(self, path: str, data: dict = None, timeout: float = 10) -> Any:
        body = json.dumps(data or {}).encode()
        req = request.Request(
            f"{self._atx_url}{path}",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        resp = request.urlopen(req, timeout=timeout)
        return json.loads(resp.read().decode())

    def implicitly_wait(self, timeout: float):
        self._implicit_wait = timeout

    @property
    def info(self) -> dict:
        return self._http_get("/info")

    @property
    def serial(self) -> str:
        try:
            info = self.info
            return info.get("serial", "local")
        except Exception:
            return "local"

    def shell(self, cmd: str, timeout: float = 30) -> ShellResult:
        """执行 shell 命令（本地直接运行，不需要 ADB）"""
        try:
            result = subprocess.run(
                cmd, shell=True, capture_output=True, text=True,
                timeout=timeout,
            )
            return ShellResult(result.stdout + result.stderr, result.returncode)
        except subprocess.TimeoutExpired:
            return ShellResult("", -1)
        except Exception as e:
            return ShellResult(str(e), -1)

    def click(self, x: int, y: int):
        self._http_post("/click", {"x": x, "y": y})

    def long_click(self, x: int, y: int, duration: float = 0.5):
        self._http_post("/click", {"x": x, "y": y, "duration": duration})

    def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5):
        self._http_post("/swipe", {
            "x1": x1, "y1": y1, "x2": x2, "y2": y2,
            "duration": duration,
        })

    def swipe_ext(self, direction: str, scale: float = 0.8):
        info = self.info
        w = info.get("displayWidth", 1080)
        h = info.get("displayHeight", 2400)
        cx, cy = w // 2, h // 2
        dist_x = int(w * scale / 2)
        dist_y = int(h * scale / 2)

        moves = {
            "up": (cx, cy + dist_y, cx, cy - dist_y),
            "down": (cx, cy - dist_y, cx, cy + dist_y),
            "left": (cx + dist_x, cy, cx - dist_x, cy),
            "right": (cx - dist_x, cy, cx + dist_x, cy),
        }
        coords = moves.get(direction, moves["up"])
        self.swipe(*coords)

    def send_keys(self, text: str):
        self._http_post("/keys", {"keys": text})

    def clear_text(self):
        self._http_post("/clear")

    def press(self, key: str):
        key_map = {
            "home": 3, "back": 4, "menu": 82, "power": 26,
            "volume_up": 24, "volume_down": 25, "enter": 66,
            "recent": 187, "search": 84,
        }
        code = key_map.get(key.lower(), key)
        self.shell(f"input keyevent {code}")

    def app_start(self, package: str, activity: str = None):
        if activity:
            self.shell(f"am start -n {package}/{activity}")
        else:
            self.shell(
                f"monkey -p {package} -c android.intent.category.LAUNCHER 1"
            )

    def app_stop(self, package: str):
        self.shell(f"am force-stop {package}")

    def dump_hierarchy(self) -> str:
        result = self._http_get("/dump/hierarchy")
        return result if isinstance(result, str) else json.dumps(result)

    def screenshot(self, format: str = "raw") -> bytes:
        url = f"{self._atx_url}/screenshot/0"
        resp = request.urlopen(url, timeout=15)
        return resp.read()

    def __call__(self, **kwargs):
        """兼容 u2 的选择器语法: d(text="xxx"), d(resourceId="xxx")"""
        return _Selector(self, kwargs)


class _Selector:
    """兼容 u2 的 UI 选择器"""

    def __init__(self, device: LocalDevice, criteria: dict):
        self._device = device
        self._criteria = criteria

    def _build_xpath(self) -> str:
        parts = []
        if "text" in self._criteria:
            parts.append(f'@text="{self._criteria["text"]}"')
        if "textContains" in self._criteria:
            parts.append(f'contains(@text, "{self._criteria["textContains"]}")')
        if "textMatches" in self._criteria:
            parts.append(f'matches(@text, "{self._criteria["textMatches"]}")')
        if "resourceId" in self._criteria:
            parts.append(f'@resource-id="{self._criteria["resourceId"]}"')
        if "className" in self._criteria:
            parts.append(f'@class="{self._criteria["className"]}"')
        if "description" in self._criteria:
            parts.append(f'@content-desc="{self._criteria["description"]}"')

        if parts:
            return f'//*[{" and ".join(parts)}]'
        return '//*'

    def exists(self, timeout: float = None) -> bool:
        t = timeout if timeout is not None else self._device._implicit_wait
        end = time.time() + t
        while time.time() < end:
            try:
                hierarchy = self._device.dump_hierarchy()
                xpath = self._build_xpath()
                if self._match_in_hierarchy(hierarchy, xpath):
                    return True
            except Exception:
                pass
            time.sleep(0.5)
        return False

    def click(self, timeout: float = None):
        t = timeout if timeout is not None else self._device._implicit_wait
        end = time.time() + t
        while time.time() < end:
            try:
                resp = self._device._http_post("/xpath", {
                    "xpath": self._build_xpath(),
                    "action": "click",
                })
                return resp
            except Exception:
                pass
            time.sleep(0.5)
        raise TimeoutError(f"Element not found: {self._criteria}")

    def get_text(self, timeout: float = None) -> str:
        t = timeout if timeout is not None else self._device._implicit_wait
        end = time.time() + t
        while time.time() < end:
            try:
                resp = self._device._http_post("/xpath", {
                    "xpath": self._build_xpath(),
                    "action": "get_text",
                })
                return resp.get("text", "")
            except Exception:
                pass
            time.sleep(0.5)
        return ""

    @staticmethod
    def _match_in_hierarchy(hierarchy: str, xpath: str) -> bool:
        try:
            import xml.etree.ElementTree as ET
            root = ET.fromstring(hierarchy) if isinstance(hierarchy, str) else hierarchy
            return len(root.findall(f".{xpath}")) > 0
        except Exception:
            import re
            if '@text=' in xpath:
                text = xpath.split('@text="')[1].split('"')[0]
                return text in hierarchy
            return False
