import logging
import re
import xml.etree.ElementTree as ET

from GramAddict.core.device_facade import DeviceFacade

logger = logging.getLogger(__name__)


def parse_bounds(bounds_str):
    if not bounds_str:
        return None
    nums = [int(n) for n in re.findall(r"\d+", bounds_str)]
    if len(nums) == 4:
        return nums
    return None


class InstagramEmulator:
    """
    A 1:1 State-Machine based Android Device Emulator.
    Instead of connecting to an Appium server, this class simulates Android
    by transitioning between real XML dumps when the bot taps specific coordinates.
    """

    def __init__(self, initial_state, states, transitions, images=None):
        self.current_state = initial_state
        self.states = states
        self.transitions = transitions
        self.images = images or {}

        self.pressed_keys = []
        self.clicks = []
        self.swipes = []
        self.app_starts = []
        self.app_id = "com.instagram.android"
        self._info = {
            "screenOn": True,
            "sdkInt": 30,
            "displaySizeDpX": 400,
            "displayWidth": 1080,
            "displayHeight": 2400,
        }

        # Mocking the sub-interfaces uiautomator2 uses inside GramAddict
        class _V2:
            def __init__(self_, parent):
                self_._parent = parent
                self_.info = parent._info
                self_.settings = {}

            def dump_hierarchy(self_, compressed=False):
                return self_._parent.dump_hierarchy()

            def app_current(self_):
                return {"package": "com.instagram.android"}

            def app_start(self_, package_name, **kwargs):
                self_._parent.app_starts.append(package_name)
                logger.info(f"[Emulator] app_start({package_name})")

            def app_stop(self_, package_name):
                logger.info(f"[Emulator] app_stop({package_name})")

            def app_clear(self_, package_name):
                logger.info(f"[Emulator] app_clear({package_name})")

            def window_size(self_):
                return (1080, 2400)

            def screenshot(self_):
                from PIL import Image

                img_src = self_._parent.images.get(self_._parent.current_state)
                if img_src:
                    if isinstance(img_src, str):
                        return Image.open(img_src)
                    return img_src
                return Image.new("RGB", (1, 1), color="black")

            def shell(self_, cmd):
                if isinstance(cmd, str) and cmd.startswith("input tap"):
                    parts = cmd.split()
                    try:
                        x, y = int(parts[-2]), int(parts[-1])
                        self_._parent.click(x, y)
                    except (ValueError, IndexError):
                        pass
                elif isinstance(cmd, str) and cmd.startswith("input swipe"):
                    parts = cmd.split()
                    try:
                        sx, sy, ex, ey = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5])
                        self_._parent.swipe(sx, sy, ex, ey)
                    except (ValueError, IndexError):
                        pass

            def swipe(self_, sx, sy, ex, ey, **kwargs):
                self_._parent.swipe(sx, sy, ex, ey)

            def press(self_, key):
                self_._parent.press(key)

            def long_click(self_, x, y, duration=1.5):
                # Emulator doesn't simulate long click logic differently from click yet
                self_._parent.click(x, y)

        self.deviceV2 = _V2(self)

        class _Touch:
            def __init__(self_, parent):
                self_._parent = parent

            def down(self_, x=None, y=None, **kwargs):
                if "obj" in kwargs:
                    obj = kwargs["obj"]
                    if isinstance(obj, dict) and "bounds" in obj:
                        b = obj["bounds"]
                        if isinstance(b, str):
                            nums = [int(n) for n in re.findall(r"\d+", b)]
                            x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
                        else:
                            x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
                    else:
                        x, y = (
                            int(getattr(obj, "x", getattr(obj, "x1", 0))),
                            int(getattr(obj, "y", getattr(obj, "y1", 0))),
                        )

                x_val = int(x) if x is not None else 0
                y_val = int(y) if y is not None else 0
                self_._parent.click(x_val, y_val)

            def up(self_, x=None, y=None, **kwargs):
                pass

        self.deviceV2.touch = _Touch(self)

    def dump_hierarchy(self):
        logger.info(f"[Emulator] Dumping hierarchy for state: {self.current_state}")
        return self.states[self.current_state]

    def press(self, key):
        self.pressed_keys.append(key)
        logger.info(f"[Emulator] Pressed key: {key}")

        # Handle state transitions for hardware keys (like back)
        state_trans = self.transitions.get(self.current_state, {})
        for condition, next_state in state_trans.get("press", []):
            if condition == key:
                logger.info(f"[Emulator] Transition: {self.current_state} -> {next_state}")
                self.current_state = next_state
                break

    def _validate_hitbox(self, x, y, xml_content):
        import re

        try:
            cleaned_content = re.sub(r"<\?xml[^>]*\?>", "", xml_content)
            root = ET.fromstring(f"<wrapper>{cleaned_content}</wrapper>".encode("utf-8"))
            for node in root.iter():
                bounds_str = node.attrib.get("bounds", "")
                is_actionable = (
                    node.attrib.get("clickable", "false") == "true"
                    or node.attrib.get("long-clickable", "false") == "true"
                    or node.attrib.get("scrollable", "false") == "true"
                    or node.attrib.get("focusable", "false") == "true"
                )
                if bounds_str and is_actionable:
                    bounds = parse_bounds(bounds_str)
                    if bounds and bounds[0] <= x <= bounds[2] and bounds[1] <= y <= bounds[3]:
                        return
            raise AssertionError(
                f"🛑 ZERO-TRUST VIOLATION: Bot clicked ({x}, {y}) but there is NO actionable UI element at these coordinates! "
                f"The VLM hallucinated or miscalculated bounds."
            )
        except ET.ParseError:
            pass

    def click(self, x=None, y=None, **kwargs):
        if "obj" in kwargs:
            obj = kwargs["obj"]
            if isinstance(obj, dict) and "bounds" in obj:
                b = obj["bounds"]
                if isinstance(b, str):
                    nums = [int(n) for n in re.findall(r"\d+", b)]  # noqa: F823
                    x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
                else:
                    x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
            else:
                x, y = int(getattr(obj, "x", getattr(obj, "x1", 0))), int(getattr(obj, "y", getattr(obj, "y1", 0)))

        x_val = int(x) if x is not None else 0
        y_val = int(y) if y is not None else 0
        self.clicks.append((x_val, y_val))

        logger.info(f"[Emulator] Clicked at ({x_val}, {y_val})")
        xml_content = self.states[self.current_state]
        self._validate_hitbox(x_val, y_val, xml_content)

        # Evaluate state transition
        xml_content = self.states[self.current_state]
        try:
            # We add a fake wrapper in case the XML dump has multiple roots or no root
            # But we must strip the XML declaration first if present!
            import re

            cleaned_content = re.sub(r"<\?xml[^>]*\?>", "", xml_content)
            root = ET.fromstring(f"<wrapper>{cleaned_content}</wrapper>".encode("utf-8"))
            clicked_node = None

            # Find the deepest node containing the click (smallest area)
            min_area = float("inf")
            for node in root.iter("node"):
                bounds = parse_bounds(node.attrib.get("bounds", ""))
                if bounds and bounds[0] <= x_val <= bounds[2] and bounds[1] <= y_val <= bounds[3]:
                    area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
                    if area <= min_area:
                        min_area = area
                        clicked_node = node

            if clicked_node is not None:
                desc = clicked_node.attrib.get("content-desc", "")
                res_id = clicked_node.attrib.get("resource-id", "")

                # Check transitions
                state_trans = self.transitions.get(self.current_state, {})
                for condition, next_state in state_trans.get("clicks", []):
                    if condition.get("desc") and condition["desc"] in desc:
                        logger.info(
                            f"[Emulator] Transition based on desc='{desc}': {self.current_state} -> {next_state}"
                        )
                        self.current_state = next_state
                        break
                    if condition.get("id") and condition["id"] == res_id:
                        logger.info(
                            f"[Emulator] Transition based on id='{res_id}': {self.current_state} -> {next_state}"
                        )
                        self.current_state = next_state
                        break
        except Exception as e:
            logger.error(f"[Emulator] Failed to parse XML for transition logic: {e}")
            logger.error(f"[Emulator] XML prefix: {repr(self.states[self.current_state][:100])}")

    def swipe(self, sx, sy, ex, ey, **kwargs):
        self.swipes.append({"start": (sx, sy), "end": (ex, ey)})
        logger.info(f"[Emulator] Swiped from ({sx}, {sy}) to ({ex}, {ey})")
        # Could implement swipe transitions here (e.g. scroll down loads new feed)

    def app_start(self, pkg, use_monkey=False):
        self.app_starts.append(pkg)
        logger.info(f"[Emulator] app_start({pkg})")

    def app_stop(self, pkg):
        pass

    def unlock(self):
        pass

    def shell(self, cmd):
        pass

    def cm_to_pixels(self, cm):
        return int(cm * 40)

    def get_screenshot_b64(self):
        return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="

    def get_info(self):
        return self._info


def create_emulator_facade(initial_state, states, transitions, monkeypatch, images=None):
    """
    Returns a DeviceFacade that is backed by our InstagramEmulator
    instead of UIAutomator2.
    """
    emulator = InstagramEmulator(initial_state, states, transitions, images=images)

    # We must patch u2.connect to avoid ConnectError during DeviceFacade init
    from GramAddict.core import device_facade

    class WatcherStub:
        def __call__(self, name):
            return self

        def when(self, xpath=None, **kwargs):
            return self

        def click(self):
            return self

        def start(self):
            pass

    emulator.deviceV2.watcher = WatcherStub()

    monkeypatch.setattr(device_facade.u2, "connect", lambda *args, **kwargs: emulator.deviceV2)

    facade = DeviceFacade("emulator", "com.instagram.android", None)

    return facade, emulator
