"""
AutoPOE2 - Bộ Điều Phối Vòng Đời Tự Hành Toàn Diện (Autonomous Lifecycle Manager)
Hiện thực hóa triết lý vận hành tự động 1-Click:
- Giai đoạn 1 (Chưa End Game): Tự động mở game -> đăng nhập -> cày cuốc, làm nhiệm vụ chiến dịch từ Lv 1 đến Endgame.
- Giai đoạn 2 (Đã Đạt End Game): Tự động chuyển sang chế độ Earning (farm map, nhặt tiền tệ, lọc đồ qua The Hooded One, săn kèo arbitrage).
"""

import enum
import logging
import os
import threading
import time
from typing import Any, Callable, Dict, List, Optional, Tuple

logger = logging.getLogger("AutonomousLifecycle")


class LifecyclePhase(enum.Enum):
    STANDBY = "STANDBY"
    AUTO_LAUNCH_CLIENT = "AUTO_LAUNCH_CLIENT"
    AUTO_LOGIN = "AUTO_LOGIN"
    CAMPAIGN_GRIND = "CAMPAIGN_GRIND"       # Giai đoạn 1: Cày cuốc & Làm nhiệm vụ đến Endgame
    ENDGAME_EARNING = "ENDGAME_EARNING"     # Giai đoạn 2: Earning, Farm Map & Market Arbitrage


class EndgameMappingSubState(enum.Enum):
    """Trạng thái chi tiết của chu trình cày map khép kín tại Giai đoạn 2 (Cold Path Orchestrator)."""
    IDLE = "IDLE"                               # Chưa kích hoạt hoặc đã hoàn tất chỉ tiêu max_maps
    PRECHECK_INVENTORY = "PRECHECK_INVENTORY"   # Quét 60 ô túi đồ, kiểm tra độ đầy, kích hoạt Hooded One nếu cần
    SELECT_WAYSTONE = "SELECT_WAYSTONE"         # Quét 12 ô Hàng 1 (Row 0) tìm Waystone khả dụng & Tablet (Row 1)
    DISPATCH_MAP_DEVICE = "DISPATCH_MAP_DEVICE" # Phát lệnh Opcode 7 (EXECUTE_MAP_DEVICE_ROUTINE) qua CoreShmBridge
    WAIT_ZONE_ENTER = "WAIT_ZONE_ENTER"         # Lắng nghe Telemetry/Log chờ C++ Core hoàn tất routine và vào Map
    IN_MAP_MONITORING = "IN_MAP_MONITORING"     # Giám sát C++ Core cày cuốc, né chiêu, săn boss trong Map
    MAP_COMPLETED = "MAP_COMPLETED"             # Nhân vật quay về Hideout an toàn, ghi nhận thành tích & cooldown
    EXHAUSTED = "EXHAUSTED"                     # Hết Waystone trong túi đồ, dừng an toàn tại Hideout


try:
    from src.assistant_tool.core_shm_bridge import CharacterLifecycleState
except ImportError:
    from core_shm_bridge import CharacterLifecycleState  # type: ignore[no-redef]

# Tọa độ màn hình thực nghiệm xác thực 2560x1440 (Shoreline Hideout & Atlas UI)
MAP_DEVICE_SCREEN_POS_2560 = (1054, 735)
ATLAS_ENDGAME_TAB_POS_2560 = (1563, 95)
ATLAS_TRAVERSE_BTN_POS_2560 = (1091, 747)  # Ground-truth thực tế @2560x1440 (MapDeviceHandler SSoT 1080p: 806, 563)


class AutonomousLifecycleManager:
    """
    Quản lý vòng đời tiến hóa tự hành của nhân vật.
    Hoạt động phi tập trung (decoupled), lắng nghe snapshot Shared Memory và Log events
    để ra quyết định chuyển cảnh và giao tiếp với C++ Core Engine.
    """

    ENDGAME_LEVEL_THRESHOLD = 65

    # Các UI scene overlay mở bằng phím U hoặc giá trị không hợp lệ
    UI_SCENE_NAMES = frozenset({
        "act 1",
        "act 2",
        "act 3",
        "act 4",
        "atlas",
        "(null)",
        "(unknown)",
    })

    @classmethod
    def is_ui_scene(cls, name: str) -> bool:
        """Kiểm tra xem tên khu vực có phải là UI overlay scene không."""
        if not name:
            return True
        clean = name.strip().lower()
        if clean.startswith("đã chuyển đến khu vực:"):
            clean = clean.split(":", 1)[1].strip()
        if clean.startswith("[dnt"):
            return True
        if clean in cls.UI_SCENE_NAMES:
            return True
        return clean in {"world map", "[dnt] world map"}

    def __init__(self, core_bridge=None, core_controller=None, config: Optional[Dict[str, Any]] = None):
        self.core_bridge = core_bridge
        self.core_controller = core_controller
        self.config = config or {}

        self.phase = LifecyclePhase.STANDBY
        self.is_active = False
        self._lock = threading.Lock()
        self._stop_event = threading.Event()
        self._eval_thread: Optional[threading.Thread] = None

        # Trạng thái theo dõi
        self.current_area_code = ""
        self.current_area_name = ""
        self.player_level = 1
        self.is_in_town = False  # KHÔNG ĐƯỢC mặc định là True khi chưa có dữ liệu log xác thực!
        self.has_real_map_log = False  # Đã xác thực khu vực chơi thật (Client.txt SCENE hoặc SHM area_name từ LogSensor)
        self.last_travel_time = 0.0
        self.travel_cooldown_sec = 8.0  # Chống spam lệnh chuyển map
        self.last_launch_attempt = 0.0
        self.last_core_start_attempt = 0.0
        self.current_objective_desc = "Đang khởi tạo hệ thống..."

        # Các module thành phần
        from src.assistant_tool.auto_quester import AutoQuester
        from src.assistant_tool.optical_inventory_scanner import OpticalInventoryScanner
        from src.assistant_tool.screen_capturer import ScreenCapturer
        self.quester = AutoQuester()
        self.inventory_scanner = OpticalInventoryScanner()
        self.capturer = ScreenCapturer()
        self.last_identify_check_time = 0.0
        self.identify_cooldown_sec = 30.0

        # Quản lý vòng lặp Multi-Map (Endgame Mapping Loop)
        self.mapping_substate = EndgameMappingSubState.IDLE
        self.max_maps = int(self.config.get("max_maps", 1))
        self.maps_completed = 0
        self.current_waystone_slot: Optional[Tuple[int, int]] = None
        self.map_dispatch_time = 0.0
        self.map_dispatch_timeout_sec = 35.0
        self.map_start_time = 0.0
        self.current_farming_map_name = ""
        self.last_map_return_time = 0.0
        self.hideout_rest_cooldown_sec = 4.0
        self.lifecycle_state = CharacterLifecycleState.UNINITIALIZED_OR_LOADING
        self.portal_exit_triggered = False
        self.last_portal_exit_time = 0.0
        self.map_monsters_detected = False

        # Listeners cập nhật UI
        self._state_listeners: List[Callable[[LifecyclePhase, str], None]] = []

    def register_listener(self, cb: Callable[[LifecyclePhase, str], None]):
        """Đăng ký nhận cập nhật trạng thái vòng đời."""
        self._state_listeners.append(cb)

    def _notify_listeners(self, status_text: str):
        for cb in self._state_listeners:
            try:
                cb(self.phase, status_text)
            except Exception as e:
                logger.error(f"[Lifecycle] Lỗi callback listener: {e}")

    def start(self, config: Optional[Dict[str, Any]] = None):
        """Kích hoạt chế độ tự hành 1-Click."""
        with self._lock:
            if self.is_active:
                return
            if config:
                self.config = config
                self.max_maps = int(self.config.get("max_maps", self.max_maps))
            self.mapping_substate = EndgameMappingSubState.IDLE
            self.is_active = True
            self._stop_event.clear()
            self._eval_thread = threading.Thread(target=self._lifecycle_loop, daemon=True)
            self._eval_thread.start()
            logger.info("[Lifecycle] >> ĐÃ KÍCH HOẠT CHẾ ĐỘ TỰ HÀNH 1-CLICK <<")

    def stop(self):
        """Dừng chế độ tự hành."""
        with self._lock:
            self.is_active = False
            self._stop_event.set()
            self.phase = LifecyclePhase.STANDBY
            self._notify_listeners("Chế độ tự hành đã TẠM DỪNG.")
            logger.info("[Lifecycle] >> ĐÃ TẠM DỪNG CHẾ ĐỘ TỰ HÀNH <<")

    def on_telemetry_snapshot(self, snapshot):
        """Tiếp nhận snapshot telemetry từ CoreShmBridge."""
        if not snapshot or not snapshot.is_online:
            return

        snap_code = getattr(snapshot, "area_code", "") or ""
        snap_name = getattr(snapshot, "area_name", "") or ""

        if snap_code:
            self.current_area_code = snap_code

        # Core LogSensor ghi area_name từ Client.txt (SCENE / Generating). Tên khu vực
        # chơi thật trên SHM là nguồn xác thực — không đợi thêm một dòng log Python.
        # Chuỗi rỗng / Act 1–4 / Atlas / (null) tuyệt đối không xác thực.
        if snap_name and not self.is_ui_scene(snap_name):
            self.current_area_name = snap_name
            self.has_real_map_log = True

        raw_lvl = getattr(getattr(snapshot, "player", None), "level", None)
        if isinstance(raw_lvl, (int, float)):
            self.player_level = int(raw_lvl)
        if self.current_area_name and not self.is_ui_scene(self.current_area_name):
            if "hideout" in self.current_area_name.lower():
                cur_lvl = int(self.player_level) if isinstance(self.player_level, (int, float)) else 1
                self.player_level = max(cur_lvl, self.ENDGAME_LEVEL_THRESHOLD)
            self.is_in_town = self.quester.is_town_area(self.current_area_code, self.current_area_name)

        if hasattr(snapshot, "lifecycle_state"):
            self.lifecycle_state = snapshot.lifecycle_state

    def on_area_changed(self, area_code: str, area_name: str, area_level: int = 1):
        """Xử lý sự kiện chuyển đổi khu vực từ LogSensor."""
        clean_name = (area_name or "").strip()
        if clean_name.lower().startswith("đã chuyển đến khu vực:"):
            clean_name = clean_name.split(":", 1)[1].strip()

        # Bỏ qua các sự kiện UI overlay / scene: Act 1..4, Atlas, (null), (unknown)
        if self.is_ui_scene(clean_name):
            logger.info(f"[Lifecycle] Bỏ qua sự kiện UI scene / map không hợp lệ: '{clean_name}'")
            return

        self.current_area_code = area_code
        self.current_area_name = clean_name
        self.has_real_map_log = True
        if "hideout" in clean_name.lower() or "hideout" in (area_code or "").lower():
            self.player_level = max(self.player_level, self.ENDGAME_LEVEL_THRESHOLD)
        self.is_in_town = self.quester.is_town_area(area_code, clean_name)
        logger.info(f"[Lifecycle] Chuyển cảnh: {clean_name} ({area_code}) - Ở Làng: {self.is_in_town}")

        # Xử lý chuyển trạng thái Mapping SubState thời gian thực
        if self.phase == LifecyclePhase.ENDGAME_EARNING:
            if self.mapping_substate == EndgameMappingSubState.WAIT_ZONE_ENTER and not self.is_in_town:
                self.mapping_substate = EndgameMappingSubState.IN_MAP_MONITORING
                self.map_start_time = time.time()
                self.current_farming_map_name = clean_name
            elif self.mapping_substate == EndgameMappingSubState.IN_MAP_MONITORING and self.is_in_town:
                self.mapping_substate = EndgameMappingSubState.MAP_COMPLETED
                self.last_map_return_time = time.time()

    def evaluate_phase(self) -> LifecyclePhase:
        """
        Đánh giá giai đoạn nhân vật:
        - Nếu ở Hideout (bất kỳ hideout nào) -> ENDGAME_EARNING (tự động nâng player_level lên >= 65)
        - Nếu level >= 65 hoặc đang trong Map Device / Endgame Area -> ENDGAME_EARNING
        - Ngược lại -> CAMPAIGN_GRIND
        """
        area_code_lower = (self.current_area_code or "").lower()
        area_name_lower = (self.current_area_name or "").lower()

        # 1. Bất kỳ khu vực Hideout nào đều là Endgame Hub
        is_hideout = "hideout" in area_code_lower or "hideout" in area_name_lower
        if is_hideout:
            if self.player_level < self.ENDGAME_LEVEL_THRESHOLD:
                self.player_level = self.ENDGAME_LEVEL_THRESHOLD
            return LifecyclePhase.ENDGAME_EARNING

        # 2. Kiểm tra dấu hiệu Endgame (Map Atlas, Map Tier, level 65+)
        is_endgame_map = (
            "endgame" in area_code_lower
            or "atlas" in area_code_lower
            or ("map" in area_code_lower and "tier" in area_name_lower)
            or self.player_level >= self.ENDGAME_LEVEL_THRESHOLD
            or self.quester.is_endgame_map(self.current_area_code, self.current_area_name, self.player_level)
        )

        if is_endgame_map:
            return LifecyclePhase.ENDGAME_EARNING
        return LifecyclePhase.CAMPAIGN_GRIND

    def _lifecycle_loop(self):
        """Vòng lặp điều phối tự hành chạy độc lập chu kỳ 1.0 giây."""
        while not self._stop_event.is_set():
            try:
                self._evaluate_and_act()
            except Exception as e:
                logger.error(f"[Lifecycle Loop Error] {e}", exc_info=True)
            time.sleep(1.0)

    def _evaluate_and_act(self):
        now = time.time()

        # 1. Kiểm tra xem Game Client có đang chạy hay không
        from src.assistant_tool.client_launcher import Poe2ClientLauncher
        is_client_running = Poe2ClientLauncher.is_client_running()

        if not is_client_running:
            if self.config.get("auto_launch_client", True):
                self.phase = LifecyclePhase.AUTO_LAUNCH_CLIENT
                if now - getattr(self, "last_launch_attempt", 0.0) >= 15.0:
                    self.last_launch_attempt = now
                    exe_path = self.config.get("poe2_exe_path")
                    self._notify_listeners("Game chưa chạy -> Đang tự động mở PathOfExile.exe...")
                    threading.Thread(
                        target=Poe2ClientLauncher.launch_client,
                        args=(exe_path,),
                        kwargs={"wait_timeout_sec": 30},
                        daemon=True
                    ).start()
                else:
                    self._notify_listeners("Game chưa chạy -> Đang chờ client khởi động...")
                return
            else:
                self.phase = LifecyclePhase.STANDBY
                self._notify_listeners("Chờ người dùng mở game PathOfExile.exe...")
                return

        # 2. Đảm bảo Core C++ Engine đang chạy (Cooldown ít nhất 5 giây giữa các lần gọi start)
        if self.core_controller and not self.core_controller.is_running():
            if now - getattr(self, "last_core_start_attempt", 0.0) >= 5.0:
                self.last_core_start_attempt = now
                self._notify_listeners("Đang khởi động C++ Core Engine 120Hz...")
                self.core_controller.start(self.config)
            return

        # 3. Đánh giá giai đoạn tiến hóa
        new_phase = self.evaluate_phase()
        self.phase = new_phase

        if self.phase == LifecyclePhase.CAMPAIGN_GRIND:
            self._execute_campaign_grind_step()
        elif self.phase == LifecyclePhase.ENDGAME_EARNING:
            self._execute_endgame_earning_step()

    def _execute_campaign_grind_step(self):
        """
        Thực thi hành động cho Giai đoạn 1: Cày cuốc & Làm nhiệm vụ.
        - Nếu ở Làng: Tự động xác định quest tiếp theo -> gửi phím U và dịch chuyển (chỉ khi mục tiêu là khu vực khác).
        - Nếu ở Phụ bản: Cập nhật mục tiêu, theo dõi Core hoàn thành quest. TUYỆT ĐỐI KHÔNG gửi lệnh travel phím U.

        CHỈ gửi send_trigger_world_map_travel khi:
        1. self.current_area_name KHÔNG rỗng và được xác nhận là Thị trấn (self.is_in_town == True).
        2. Tên map hiện tại KHÔNG PHẢI là UI scene ("act 1", "act 2", "act 3", "act 4", "atlas").
        3. Đã xác thực khu vực chơi thật (self.has_real_map_log): Client.txt SCENE
           hoặc SHM area_name do C++ LogSensor ghi từ cùng nhật ký.
        """
        cur_name = (self.current_area_name or "").strip()
        cur_code = (self.current_area_code or "").strip()

        # INTERLOCK AN TOAN: Neu dang o Hideout, tuyet doi khong bao gio chay Campaign Grind
        if "hideout" in cur_name.lower() or "hideout" in cur_code.lower():
            self.phase = LifecyclePhase.ENDGAME_EARNING
            self.player_level = max(self.player_level, self.ENDGAME_LEVEL_THRESHOLD)
            self._notify_listeners(
                f"🛡️ [LIFECYCLE] Nhan vat tai Hideout ({self.current_area_name}) -> Chuyen sang Pha 2: ENDGAME_EARNING!"
            )
            return

        # 3 điều kiện tiên quyết bắt buộc để gửi lệnh dịch chuyển từ Làng
        can_trigger_town_travel = (
            bool(cur_name)
            and self.is_in_town
            and not self.is_ui_scene(cur_name)
            and getattr(self, "has_real_map_log", False)
        )

        obj = self.quester.resolve_active_objective(
            self.current_area_code,
            self.current_area_name,
            area_level=self.player_level
        )
        self.current_objective_desc = f"[Act {obj.act}] {obj.area_name}: {obj.target_name} ({obj.description})"

        if can_trigger_town_travel:
            # Kiểm tra nếu mục tiêu trùng với khu vực hiện tại thì không gửi lệnh travel vô nghĩa
            is_same_area = False
            cur_name_lower = cur_name.lower()
            cur_code_lower = cur_code.lower()
            obj_name = (obj.area_name or "").strip().lower()
            obj_code = (obj.area_code or "").strip().lower()

            if (cur_name_lower and obj_name and cur_name_lower == obj_name) or (cur_code_lower and obj_code and cur_code_lower == obj_code):
                is_same_area = True

            if is_same_area:
                self._notify_listeners(
                    f"🛡️ [LÀNG] Đã ở khu vực mục tiêu: {self.current_area_name} -> Tiến hành thực thi nhiệm vụ: {obj.target_name}"
                )
                return

            now = time.time()
            if now - self.last_travel_time > self.travel_cooldown_sec:
                self._notify_listeners(
                    f"🛡️ [LÀNG - TỰ HÀNH QUEST] Mở World Map 'U' -> Dịch chuyển tới: {obj.area_name} (Act {obj.act})"
                )
                # Gửi lệnh mở bản đồ và dịch chuyển
                if self.core_bridge and hasattr(self.core_bridge, "send_trigger_world_map_travel"):
                    success = self.core_bridge.send_trigger_world_map_travel(obj.act, obj.zone_index)
                    if success:
                        self.last_travel_time = now
                        logger.info(f"[Lifecycle] Đã gửi lệnh World Map Travel tới Act {obj.act}, Node {obj.zone_index}")
            else:
                self._notify_listeners(
                    f"🛡️ [LÀNG] Đang chuẩn bị chuyển cảnh sang: {obj.area_name}..."
                )
        elif self.is_in_town:
            # Được đánh dấu là ở làng nhưng chưa thỏa mãn đủ 3 điều kiện (ví dụ chưa có log map thật hoặc là UI scene)
            if not getattr(self, "has_real_map_log", False) or not cur_name:
                self._notify_listeners(
                    "⏳ [CHỜ DỮ LIỆU GAME] Đang chờ xác thực vị trí bản đồ từ Log Client..."
                )
            else:
                self._notify_listeners(
                    f"🛡️ [LÀNG] Bản đồ hiện tại ({self.current_area_name}) đang ở trạng thái chờ xác thực."
                )
        else:
            # TUYỆT ĐỐI KHÔNG gửi send_trigger_world_map_travel khi đang ở map chiến đấu hoặc chưa có dữ liệu log xác thực
            if not getattr(self, "has_real_map_log", False) and not cur_name:
                self._notify_listeners(
                    "⏳ [CHỜ DỮ LIỆU GAME] Đang chờ xác thực vị trí bản đồ từ Log Client..."
                )
            else:
                self._notify_listeners(
                    f"⚔️ [CHIẾN ĐẤU] Đang cày cuốc & thực hiện nhiệm vụ: {obj.target_name} ({obj.area_name})"
                )

    def _execute_endgame_earning_step(self):
        """
        Giai đoạn 2: Điều phối chu trình Mapping khép kín (Closed-Loop Mapping Engine).
        Phối hợp bất đối xứng: Python Cold Path kiểm soát kho đồ & điều phối cấp cao,
        C++ Core Hot Path 120Hz trực tiếp thực thi Map Device routine và in-map clearing.
        """
        try:
            from src.assistant_tool.auto_quester import MapScenario
        except ImportError:
            from auto_quester import MapScenario

        raw = self.config.get("map_scenario", MapScenario.FAST_CLEAR)
        try:
            scenario = MapScenario(int(raw))
        except (TypeError, ValueError):
            scenario = MapScenario.FAST_CLEAR

        auto_portal = bool(self.config.get("auto_portal_after_boss", True))
        if hasattr(self, "quester") and self.quester:
            self.quester.set_map_scenario(scenario, auto_portal)

        if hasattr(self, "core_bridge") and self.core_bridge and hasattr(self.core_bridge, "send_set_map_scenario"):
            self.core_bridge.send_set_map_scenario(int(scenario), auto_portal)

        names = {
            MapScenario.BOSS_RUSH: "BOSS_RUSH",
            MapScenario.ATLAS_QUEST_RUSH: "ATLAS_QUEST",
            MapScenario.FAST_CLEAR: "FAST_CLEAR",
            MapScenario.FULL_CLEAR: "FULL_CLEAR",
        }
        label = names.get(scenario, "FAST_CLEAR")

        now = time.time()

        # ==========================================================
        # STATE MACHINE: Endgame Mapping Cycle SubState
        # ==========================================================
        if self.mapping_substate == EndgameMappingSubState.IDLE:
            if self.is_in_town or self.lifecycle_state == CharacterLifecycleState.HIDEOUT_IDLE:
                if self.maps_completed < self.max_maps:
                    self.mapping_substate = EndgameMappingSubState.PRECHECK_INVENTORY
                    self._notify_listeners(f"💰 [MAPPING #{self.maps_completed + 1}/{self.max_maps}] Bắt đầu chu trình map mới...")
                else:
                    self._notify_listeners(f"🎉 [MAPPING] Đã hoàn thành chỉ tiêu {self.maps_completed}/{self.max_maps} maps. Nghỉ an toàn tại Hideout.")
            else:
                # Nhân vật đang ở trong map khi kích hoạt
                self.mapping_substate = EndgameMappingSubState.IN_MAP_MONITORING
                self.map_start_time = now
                self.current_farming_map_name = self.current_area_name

        elif self.mapping_substate == EndgameMappingSubState.PRECHECK_INVENTORY:
            self.current_objective_desc = f"Precheck túi đồ Map #{self.maps_completed + 1}/{self.max_maps}"
            latest_cap = self.config.get("last_inventory_capture_path", "")
            if not latest_cap or not os.path.exists(latest_cap):
                latest_cap = self.capturer.capture_screenshot(event_name="PRECHECK_INV", capture_ram=False)

            if latest_cap and os.path.exists(latest_cap):
                if self.config.get("enable_hooded_one_identify", True) and (now - self.last_identify_check_time > self.identify_cooldown_sec):
                    identify_res = self.inventory_scanner.check_and_trigger_hooded_one_identify(
                        latest_cap,
                        self.core_bridge,
                        area_name=self.current_area_name,
                        monster_count=0
                    )
                    if identify_res.get("triggered"):
                        self.last_identify_check_time = now
                        self._notify_listeners(f"🧙 [HOODED ONE] Túi đồ đầy ({identify_res['occupied_slots']}/60) -> Đã gửi lệnh Identify All (Opcode 8)!")
                        return
            self.mapping_substate = EndgameMappingSubState.SELECT_WAYSTONE

        elif self.mapping_substate == EndgameMappingSubState.SELECT_WAYSTONE:
            self.current_objective_desc = f"Lựa chọn Waystone cho Map #{self.maps_completed + 1}"
            latest_cap = self.config.get("last_inventory_capture_path", "")
            if not latest_cap or not os.path.exists(latest_cap):
                latest_cap = getattr(self.capturer, "last_capture_path", "")

            waystone_slot = None
            if latest_cap and os.path.exists(latest_cap):
                waystones = self.inventory_scanner.scan_row1_waystones(latest_cap)
                if waystones:
                    sel_col, sel_cx, sel_cy, sel_rgb = waystones[0]
                    waystone_slot = (sel_col, 0)
                    self._notify_listeners(f"🎯 [MAPPING] Phát hiện Waystone tại Slot [{sel_col}, 0] (AvgRGB={sel_rgb:.1f})")
                else:
                    self.mapping_substate = EndgameMappingSubState.EXHAUSTED
                    self._notify_listeners(f"🛑 [MAPPING] Cạn kiệt Waystone trong Hàng 1 túi đồ sau {self.maps_completed} maps! Dừng an toàn.")
                    return
            else:
                # Không có ảnh quét -> Trao quyền cho C++ Core autoScanFirstRow (col=0, row=0)
                waystone_slot = (0, 0)

            self.current_waystone_slot = waystone_slot
            self.mapping_substate = EndgameMappingSubState.DISPATCH_MAP_DEVICE

        elif self.mapping_substate == EndgameMappingSubState.DISPATCH_MAP_DEVICE:
            # 1. Anti-spam & Core state interlock: C++ Core đang tiếp cận / kích hoạt / vào portal -> Không gửi lại Opcode 7
            if self.lifecycle_state in (
                CharacterLifecycleState.HIDEOUT_MAP_DEVICE_APPROACH,
                CharacterLifecycleState.HIDEOUT_MAP_DEVICE_ACTIVATE,
                CharacterLifecycleState.HIDEOUT_ENTER_PORTAL,
            ):
                self.map_dispatch_time = now
                self.mapping_substate = EndgameMappingSubState.WAIT_ZONE_ENTER
                self._notify_listeners("⏳ [MAP DEVICE] C++ Core đang thực thi Map Device routine -> Chuyển sang WAIT_ZONE_ENTER.")
                return

            # Chống spam gửi lệnh liên tục nếu vừa gửi cách đây < 5s
            if (now - self.map_dispatch_time) < 5.0:
                return

            # 2. Kiểm tra khoảng cách nhân vật tới Map Device (nếu có telemetry RAM)
            dist_to_device = self.get_map_device_distance()
            if dist_to_device is not None:
                self._notify_listeners(f"📍 [MAP DEVICE] Cự ly tới Map Device: {dist_to_device:.1f} units.")

            col, row = self.current_waystone_slot or (0, 0)
            use_tablet = bool(self.config.get("use_precursor_tablet", False))
            if self.core_bridge and hasattr(self.core_bridge, "send_map_device"):
                success = self.core_bridge.send_map_device(
                    waystone_col=col,
                    waystone_row=row,
                    use_tablet=use_tablet,
                    timeout_ms=35000
                )
                if success:
                    self.map_dispatch_time = now
                    self.mapping_substate = EndgameMappingSubState.WAIT_ZONE_ENTER
                    self._notify_listeners(
                        f"🚀 [MAP DEVICE] Đã gửi Opcode 7 (Waystone [{col},{row}], Tablet={use_tablet}) -> C++ Core tiếp cận & kích hoạt cổng!"
                    )
                else:
                    self._notify_listeners("⚠️ [MAP DEVICE] Không thể gửi lệnh qua SHM. Đang thử lại...")
            else:
                self._notify_listeners("⚠️ [MAP DEVICE] Cầu nối CoreShmBridge chưa sẵn sàng!")

        elif self.mapping_substate == EndgameMappingSubState.WAIT_ZONE_ENTER:
            self.current_objective_desc = f"Chờ C++ Core mở cổng và bước vào Map #{self.maps_completed + 1}..."
            in_map = (
                (not self.is_in_town and self.current_area_name and not self.is_ui_scene(self.current_area_name))
                or (self.lifecycle_state in (CharacterLifecycleState.MAP_LOADING_GRACE, CharacterLifecycleState.MAP_FARMING_COMBAT))
            )
            if in_map:
                self.mapping_substate = EndgameMappingSubState.IN_MAP_MONITORING
                self.map_start_time = now
                self.portal_exit_triggered = False
                self.map_monsters_detected = False
                self.current_farming_map_name = self.current_area_name or "Endgame Map"
                self._notify_listeners(f"⚔️ [IN-MAP #{self.maps_completed + 1}] Đã vào Map: {self.current_farming_map_name}!")
                return

            if now - self.map_dispatch_time > self.map_dispatch_timeout_sec:
                self._notify_listeners("⚠️ [MAP DEVICE] Quá thời gian chờ vào Map (35s). Đang kiểm tra lại trạng thái...")
                self.mapping_substate = EndgameMappingSubState.PRECHECK_INVENTORY

        elif self.mapping_substate == EndgameMappingSubState.IN_MAP_MONITORING:
            duration = now - self.map_start_time
            self.current_objective_desc = f"Đang dọn Map #{self.maps_completed + 1}: {self.current_farming_map_name} ({label}, {int(duration)}s)"

            # Theo dõi số lượng quái vật còn lại từ telemetry
            snap = getattr(self.core_bridge, "last_snapshot", None) if self.core_bridge else None
            try:
                monsters_cnt = int(getattr(snap, "monsters_count", 0))
            except (ValueError, TypeError):
                monsters_cnt = 0
            if monsters_cnt > 0:
                self.map_monsters_detected = True

            # Điều kiện map hoàn tất:
            # 1. C++ Core báo MAP_COMPLETED_PORTAL_OUT
            # 2. Quái vật đã dọn sạch (đã từng phát hiện quái, nay còn 0 và map đã chạy ít nhất 15s)
            # 3. Quá thời gian quy định (map_duration_sec hoặc timeout mặc định 180s)
            map_timeout = float(self.config.get("map_duration_sec", 180.0))
            is_cleared = (
                (self.lifecycle_state == CharacterLifecycleState.MAP_COMPLETED_PORTAL_OUT)
                or (self.map_monsters_detected and monsters_cnt == 0 and duration >= 15.0)
                or (duration >= map_timeout)
            )

            if is_cleared and not self.portal_exit_triggered:
                self._notify_listeners(
                    f"🏆 [MAP CLEAR] Bản đồ đã dọn sạch hoặc đạt thời lượng ({int(duration)}s) -> Kích hoạt Town Portal!"
                )
                self.send_town_portal_exit()

            is_returned = (
                self.is_in_town
                or (duration > 5.0 and self.lifecycle_state in (CharacterLifecycleState.HIDEOUT_IDLE, CharacterLifecycleState.HIDEOUT_STASHING))
            )
            if is_returned:
                self.portal_exit_triggered = False
                self.map_monsters_detected = False
                self.mapping_substate = EndgameMappingSubState.MAP_COMPLETED
                self.last_map_return_time = now
            else:
                self._notify_listeners(f"⚔️ [IN-MAP #{self.maps_completed + 1}] {self.current_farming_map_name} | {label} ({int(duration)}s)")

        elif self.mapping_substate == EndgameMappingSubState.MAP_COMPLETED:
            self.maps_completed += 1
            duration = (self.last_map_return_time - self.map_start_time) if self.map_start_time > 0 else 0.0
            self._notify_listeners(f"✅ [HOÀN THÀNH #{self.maps_completed}/{self.max_maps}] {self.current_farming_map_name} ({duration:.1f}s)!")
            if self.maps_completed >= self.max_maps:
                self.mapping_substate = EndgameMappingSubState.IDLE
                self._notify_listeners(f"🎉 [MAPPING] Hoàn thành chỉ tiêu cày {self.maps_completed}/{self.max_maps} maps. Tạm dừng an toàn.")
            else:
                if now - self.last_map_return_time >= self.hideout_rest_cooldown_sec:
                    self.mapping_substate = EndgameMappingSubState.PRECHECK_INVENTORY
                    self._notify_listeners(f"⏳ [HIDEOUT] Nghỉ ngơi xong -> Chuẩn bị cho Map {self.maps_completed + 1}/{self.max_maps}...")
                else:
                    remain = self.hideout_rest_cooldown_sec - (now - self.last_map_return_time)
                    self._notify_listeners(f"⏳ [HIDEOUT] Nghỉ ngơi hồi phục ({remain:.1f}s)...")

        elif self.mapping_substate == EndgameMappingSubState.EXHAUSTED:
            self._notify_listeners(f"🛑 [HẾT WAYSTONE] Chu trình mapping tạm dừng sau {self.maps_completed} maps.")

    def trigger_hooded_one_identify(self, image_path: str) -> Dict[str, Any]:
        """Kích hoạt kiểm tra và gửi lệnh The Hooded One Identify All thủ công hoặc theo yêu cầu (P1-5)."""
        return self.inventory_scanner.check_and_trigger_hooded_one_identify(
            image_path=image_path,
            core_bridge=self.core_bridge,
            area_name=self.current_area_name,
            monster_count=0
        )

    def get_map_device_distance(self) -> Optional[float]:
        """Truy vấn khoảng cách từ nhân vật tới Map Device nếu có dữ liệu thực thể RAM trong snapshot."""
        if not self.core_bridge:
            return None
        snap = getattr(self.core_bridge, "last_snapshot", None)
        if not snap or not getattr(snap, "is_online", False):
            return None
        entities = getattr(snap, "entities", [])
        if not isinstance(entities, (list, tuple)):
            return None
        for ent in entities:
            if not isinstance(ent, dict):
                continue
            name = str(ent.get("name", "")).lower()
            etype = ent.get("type", 0)
            if etype in (3, 4) and ("map device" in name or "device" in name or "atlas" in name):
                try:
                    return float(ent.get("dist", 0.0))
                except (ValueError, TypeError):
                    return None
        return None

    def send_town_portal_exit(self, timeout_ms: int = 5000) -> bool:
        """Gửi lệnh mở Town Portal (Key '.' / VK_OEM_PERIOD 0xBE / Opcode 4 CAST_EMERGENCY_PORTAL) để thoát Map về Hideout."""
        self.portal_exit_triggered = True
        self.last_portal_exit_time = time.time()
        success = False
        if self.core_bridge and hasattr(self.core_bridge, "send_cast_emergency_portal"):
            success = self.core_bridge.send_cast_emergency_portal(timeout_ms=timeout_ms)
            self._notify_listeners("🌀 [TOWN PORTAL] Đã phát lệnh Opcode 4 (Key '.' / VK_OEM_PERIOD 0xBE) -> Mở Portal về Hideout!")
        elif self.core_bridge and hasattr(self.core_bridge, "send_portal"):
            success = self.core_bridge.send_portal()
            self._notify_listeners("🌀 [TOWN PORTAL] Đã gửi lệnh mở Portal qua bridge!")
        else:
            self._notify_listeners("⚠️ [TOWN PORTAL] Cầu nối CoreShmBridge không hỗ trợ send_cast_emergency_portal!")
        return success


