import gzip
import subprocess
from datetime import datetime
from functools import wraps

import cv2
import numpy as np
from adbutils import AdbClient, AdbDevice
from adbutils.errors import AdbConnectionError, AdbError

from mower import __system__
from mower.utils import config
from mower.utils.csleep import MowerExit, csleep
from mower.utils.device.emulator import restart_emulator
from mower.utils.device.exception import EmulatorError
from mower.utils.image import bytes2img
from mower.utils.log import logger

from ..utils import subprocess_run


class ADBServerError(Exception):
    pass


def retry_adb(func):
    @wraps(func)
    def retry_wrapper(self, *args, **kwargs):
        for _ in range(3):
            try:
                return func(self, *args, **kwargs)
            except MowerExit:
                raise
            except ADBServerError as e:
                logger.debug_exception(e)
                self.kill_server()
            except EmulatorError as e:
                logger.debug_exception(e)
                restart_emulator()

                self._adb_device = None
            except Exception as e:
                logger.debug_exception(e)

    return retry_wrapper


class ADB:
    def __init__(self) -> None:
        self.adb_bin = config.conf.maa_adb_path
        self._adb_client = None  # 用于存储 AdbClient 实例
        self._adb_device = None  # 用于存储 AdbDevice 实例
        self.check_server_status()

    def get_adb_client(self) -> AdbClient:
        """
        Returns an AdbClient instance.
        Will retry if connection fails.
        """
        if self._adb_client is None:
            try:
                self.check_server_status()
                self._adb_client = AdbClient("127.0.0.1", config.adb_server_port, 10)
            except Exception as e:
                raise ADBServerError(e)
        return self._adb_client

    @retry_adb
    def get_adb(self) -> AdbDevice:
        """
        Returns an AdbDevice instance.
        Will retry if connection fails.
        """
        if self._adb_device is None:
            adb_client = self.get_adb_client()
            try:
                devices = adb_client.list()
            except Exception as e:
                raise ADBServerError(e)
            if config.conf.adb not in [device.serial for device in devices]:
                try:
                    adb_client.connect(config.conf.adb, 10)
                except Exception as e:
                    raise EmulatorError(e)
            elif any(
                device.serial == config.conf.adb and device.state == "offline"
                for device in devices
            ):
                try:
                    adb_client.disconnect(config.conf.adb)
                    adb_client.connect(config.conf.adb, 10)
                except Exception as e:
                    raise EmulatorError(e)
            self._adb_device = AdbDevice(adb_client, config.conf.adb)
            if self._adb_device is None:
                raise EmulatorError("设备未连接")
        self.check_device_status()
        return self._adb_device

    @property
    def adb(self) -> AdbDevice:
        """
        Explicit property to get ADB device.
        """
        return self.get_adb()

    @property
    def adb_client(self) -> AdbClient:
        """
        Explicit property to get ADB client.
        """
        return self.get_adb_client()

    @retry_adb
    def adb_command(self, cmd, timeout=20):
        """
        Execute ADB commands in a subprocess,
        usually to be used when pulling or pushing large files.

        Args:
            cmd (list):
            timeout (int):

        Returns:
            str:
        """
        try:
            cmd = list(map(str, cmd))
            cmd = [self.adb_bin, "-P", str(config.adb_server_port)] + cmd
            return subprocess_run(cmd, timeout=timeout)
        except Exception as e:
            logger.exception(e)
            self.adb

    def check_server_status(self) -> str:
        """Use `adb devices` as `adb start-server`, result is actually useless"""
        logger.info(f"adb-server:127.0.0.1:{config.adb_server_port}")
        stdout = self.adb_command(["devices"]).strip()
        logger.info(stdout)
        return stdout

    def kill_server(self) -> None:
        self.adb_command(["kill-server"])
        self._adb_client = self._adb_device = None

    def adb_disconnect(self):
        self.adb_client.disconnect(config.conf.adb, raise_error=False)
        self._adb_device = None

    def adb_shell(
        self, cmd, stream=False, timeout=20, encoding: str | None = "utf-8", rstrip=True
    ):
        """
        Equivalent to `adb -P <port> shell <*cmd>`
        Args:
            cmd (list, str):
            stream (bool): Return stream instead of string output (Default: False)
            timeout (int): (Default: 10)
            rstrip (bool): Strip the last empty line (Default: True)

        Returns:
            str if stream=False
            socket if stream=True
        """
        if not isinstance(cmd, str):
            cmd = list(map(str, cmd))
        return self.adb.shell(
            cmd, stream=stream, timeout=timeout, encoding=encoding, rstrip=rstrip
        )

    def adb_push(self, file_path: str, target: str):
        """push file into device with adb_bin"""
        self.adb.push(file_path, target)

    def check_device_status(self):
        try:
            self._adb_device.shell("true")
        except AdbConnectionError:
            raise ADBServerError("无法连接到adb server")
        except AdbError:
            raise EmulatorError(f"无法连接到{config.conf.adb}")

    def process(
        self, path: str, args: list[str] = [], stderr: int = subprocess.DEVNULL
    ) -> subprocess.Popen:
        logger.debug(f"{path=} {args=}")
        cmd = [self.adb_bin, "-P", str(config.adb_server_port), "shell", path] + args
        return subprocess.Popen(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=stderr,
            creationflags=subprocess.CREATE_NO_WINDOW if __system__ == "windows" else 0,
        )

    def capture_display(self) -> np.ndarray:
        command = "screencap 2>/dev/null | gzip -1"
        resp = self.adb_shell(command, encoding=None)
        data = gzip.decompress(resp)
        array = np.frombuffer(data[-1920 * 1080 * 4 :], np.uint8).reshape(1080, 1920, 4)
        return cv2.cvtColor(array, cv2.COLOR_RGBA2RGB)

    def launch(self) -> None:
        """launch the application"""
        tap = config.conf.tap_to_launch_game.enable
        x = config.conf.tap_to_launch_game.x
        y = config.conf.tap_to_launch_game.y

        if tap:
            self.adb_shell(f"input tap {x} {y}")
        else:
            self.adb_shell(
                f"monkey --pct-syskeys 0 -p {config.conf.APPNAME} -c android.intent.category.LAUNCHER 1"
            )

    def exit(self) -> None:
        """exit the application"""
        self.adb_shell(f"am force-stop {config.conf.APPNAME}")

    def home(self) -> None:
        """back to home"""
        self.adb_shell(
            "am start -a android.intent.action.MAIN -c android.intent.category.HOME"
        )

    def current_focus(self) -> str:
        """detect current focus app"""
        command = "dumpsys window | grep mCurrentFocus"
        line = self.adb_shell(command)
        return line.strip()[:-1].split(" ")[-1]

    def check_current_focus(self) -> bool:
        """check if the application is in the foreground"""
        update = False
        start_time = datetime.now()
        while True:
            try:
                focus = self.current_focus()
                if focus not in [
                    f"{config.conf.APPNAME}/{config.APP_ACTIVITY_NAME}",
                    "com.hypergryph.arknights.bilibili/com.gsc.welcome.WelcomeActivity",
                ]:
                    if (datetime.now() - start_time).total_seconds() > 40:
                        self.exit()  # 应用卡死
                        start_time = datetime.now()
                    self.launch()
                    update = True
                    csleep(1)
                    continue
                return update
            except MowerExit:
                raise
            except Exception as e:
                logger.exception(e)
                update = True

    def check_device_screen(self) -> bool:
        """检查分辨率和DPI

        Returns:
            设置是否正确
        """
        res_output = self.adb_shell("wm size").strip()
        dpi_output = self.adb_shell("wm density").strip()
        logger.debug(f"{res_output} {dpi_output}".replace("\n", " "))

        physical_res, _, override_res = res_output.partition("Override")
        physical_dpi, _, override_dpi = dpi_output.partition("Override")
        res = (override_res or physical_res).partition("size:")[2].strip()
        dpi = (override_dpi or physical_dpi).partition("density:")[2].strip()
        if (
            config.conf.emulator.name == "MuMu12"
            and config.conf.screencap_strategy.startswith("droidcast")
        ):
            from mower.utils.device.method.mumumanager import MuMuManager

            if MuMuManager().app_kept_alive():
                logger.error("MuMu12的droidcast截图策略下，需要关闭应用后台保活")
                try:
                    MuMuManager().set_app_kept_alive_false()
                    logger.info("关闭应用后台保活成功")
                    restart_emulator()
                    self._adb_device = None
                except Exception as e:
                    logger.debug_exception(e)
                    raise MowerExit

        if res in ["1920x1080", "1080x1920"] and dpi == "280":
            return True
        logger.error("mower-ng仅支持1920x1080分辨率、280DPI，请修改模拟器设置")
        return False

    def get_adb_while_restart_emulator(self) -> AdbDevice:
        """
        use for restart emulator
        """
        if self._adb_device is None:
            adb_client = self.get_adb_client()
            try:
                devices = adb_client.list()
            except Exception as e:
                raise ADBServerError(e)
            if config.conf.adb not in [device.serial for device in devices]:
                try:
                    adb_client.connect(config.conf.adb, 10)
                except Exception as e:
                    raise EmulatorError(e)
            elif any(
                device.serial == config.conf.adb and device.state == "offline"
                for device in devices
            ):
                try:
                    adb_client.disconnect(config.conf.adb)
                    adb_client.connect(config.conf.adb, 10)
                except Exception as e:
                    raise EmulatorError(e)
            self._adb_device = AdbDevice(adb_client, config.conf.adb)
            if self._adb_device is None:
                raise EmulatorError("设备未连接")
        self.check_device_status()
        return self._adb_device


class DIY(ADB):
    @retry_adb
    def capture_display(self):
        try:
            command = config.conf.custom_screenshot.command
            data = subprocess.check_output(
                command,
                shell=True,
                creationflags=subprocess.CREATE_NO_WINDOW
                if __system__ == "windows"
                else 0,
            )
            return bytes2img(data)
        except Exception as e:
            logger.exception(e)
            self.adb


class ADBMultiUser(ADB):
    def start_user(self) -> None:
        """start the user"""
        self.adb_shell(f"am start-user {config.conf.mumu_multi_app.user_id}")

    def launch(self) -> None:
        """launch the application"""
        self.start_user()
        self.adb_shell(
            f"am start --user {config.conf.mumu_multi_app.user_id} {config.conf.APPNAME}/{config.APP_ACTIVITY_NAME}"
        )

    def exit(self) -> None:
        """exit the application"""
        self.adb_shell(
            f"am force-stop --user {config.conf.mumu_multi_app.user_id} {config.conf.APPNAME}"
        )

    def home(self) -> None:
        """back to home"""
        pass

    def current_focus(self) -> str:
        """detect current focus app"""
        command = f"dumpsys activity activities | grep 'mFocusedApp' | grep 'u{config.conf.mumu_multi_app.user_id}' | grep '{config.conf.APPNAME}/{config.APP_ACTIVITY_NAME}'"
        res = self.adb_shell(command)
        return res

    def check_current_focus(self) -> bool:
        """check if the application is in the foreground"""
        update = False
        start_time = datetime.now()
        while True:
            try:
                focus = self.current_focus()
                if not focus:
                    if (datetime.now() - start_time).total_seconds() > 40:
                        self.exit()  # 应用卡死
                        start_time = datetime.now()
                    self.launch()
                    update = True
                    csleep(1)
                    continue
                return update
            except MowerExit:
                raise
            except Exception as e:
                logger.exception(e)
                update = True
