import json
import os
import random
import sys
import time
import math
import ctypes
import cv2
import numpy as np
import win32con
import win32gui
from hardware_control import hw_ctrl
from game_state import parse_game_state, load_layout_config
from coordinate_patrol import CoordinatePatrol
from death_manager import DeathManager
from logistics_manager import LogisticsManager

# 定义按键常量
KEY_TAB = '3'
KEY_LOOT = '4'      # 假设你在游戏里设置了互动按键为 F
KEY_ATTACK = '2'    # 假设你的主攻击技能是 1
DEFAULT_FOOD_KEY = 'f1'     # 面包快捷键
DEFAULT_EAT_HP_THRESHOLD = 30
DEFAULT_EAT_MAX_WAIT_SEC = 30.0

# 巡逻点配置文件
WAYPOINTS_FILE = 'waypoints.json'
WIN_TITLE = "魔兽世界"

# 默认巡逻航点（waypoints.json 不存在或无效时使用）
DEFAULT_WAYPOINTS = [(23.8, 71.0), (27.0, 79.0), (31.0, 72.0)]

try:
    ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception:
    ctypes.windll.user32.SetProcessDPIAware()


def _config_base():
    """打包成 exe 时，优先从 exe 同目录读配置，否则用内嵌资源目录。"""
    if getattr(sys, 'frozen', False):
        exe_dir = os.path.dirname(sys.executable)
        if os.path.exists(exe_dir):
            return exe_dir
        return getattr(sys, '_MEIPASS', exe_dir)
    return os.path.dirname(os.path.abspath(__file__))


def get_config_path(filename):
    """解析配置文件路径：先 exe 同目录，再打包内嵌目录。"""
    base = _config_base()
    p = os.path.join(base, filename)
    if os.path.exists(p):
        return p
    if getattr(sys, 'frozen', False):
        meipass = getattr(sys, '_MEIPASS', '')
        if meipass:
            p2 = os.path.join(meipass, filename)
            if os.path.exists(p2):
                return p2
    return os.path.join(base, filename)


def move_cursor_hw(x, y, settle_sec=0.02):
    hw_ctrl.move_to(int(x), int(y))
    if settle_sec > 0:
        time.sleep(settle_sec)


def get_wow_client_rect(hwnd):
    client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd)
    screen_left, screen_top = win32gui.ClientToScreen(hwnd, (client_left, client_top))
    screen_right, screen_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom))
    return screen_left, screen_top, screen_right, screen_bottom


def load_waypoints(path=None):
    path = path or get_config_path(WAYPOINTS_FILE)
    if os.path.exists(path):
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    return []


def load_attack_loop(path):
    """从 JSON 加载攻击循环配置。支持 hp_below_percent/hp_key、mp_below_percent/mp_key（0=不启用）"""
    if not path or not os.path.exists(path):
        return None
    try:
        with open(path, 'r', encoding='utf-8') as f:
            cfg = json.load(f)
        steps = cfg.get('steps') or []
        trigger = float(cfg.get('trigger_chance', 0.3))
        hp_below = int(cfg.get('hp_below_percent', 0)) or 0
        mp_below = int(cfg.get('mp_below_percent', 0)) or 0
        return {
            'trigger_chance': trigger,
            'steps': steps,
            'hp_below_percent': hp_below,
            'hp_key': (cfg.get('hp_key') or '').strip() or None,
            'mp_below_percent': mp_below,
            'mp_key': (cfg.get('mp_key') or '').strip() or None,
        }
    except Exception:
        return None


class CursorManager:
    """通过图像识别判断鼠标图标类型"""
    def __init__(self):
        self.templates = {}
        self.handle_cache = {} # 句柄 -> 类型缓存
        self.log_path = get_config_path('cursor_recognition.log')
        self._load_templates()

    def _load_templates(self):
        # 强制使用纯相对路径，不带盘符前缀，由 Python 自动处理 CWD
        cursor_dir = get_config_path(os.path.join('images', 'cursor'))
        files = {
            'Point': 'Point.PNG',
            'Attack': 'Attack.PNG',
            'LootAll': 'LootAll.PNG',
            'Skin': 'Skin.PNG'
        }
        for name, fname in files.items():
            path = os.path.join(cursor_dir, fname)
            # 记录尝试读取的路径
            if os.path.exists(path):
                try:
                    # 使用 np.fromfile 读取为字节流，再解码，完美避开 OpenCV 的 imread 编码 Bug
                    img_array = np.fromfile(path, dtype=np.uint8)
                    img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
                    if img is not None:
                        if img.shape[2] == 3:
                            img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
                        self.templates[name] = img
                except Exception as e:
                    print(f">>> [CursorMgr] 加载 {fname} 失败: {e}")
            else:
                print(f">>> [CursorMgr] 找不到文件: {path}")

    def get_type(self, hcursor):
        if not hcursor or hcursor == 0: return 'Other', 0.0
        if hcursor in self.handle_cache: return self.handle_cache[hcursor]
        res = self._identify(hcursor)
        self.handle_cache[hcursor] = res
        return res

    def _debug_log(self, message):
        try:
            with open(self.log_path, 'a', encoding='utf-8') as f:
                f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n")
        except Exception:
            pass

    def _normalize_for_match(self, img):
        if img is None:
            return None
        if len(img.shape) == 2:
            gray = img
        elif img.shape[2] == 4:
            alpha = img[:, :, 3].astype(np.float32) / 255.0
            bgr = img[:, :, :3].astype(np.float32)
            bg = np.full_like(bgr, 255.0)
            composed = (bgr * alpha[..., None]) + (bg * (1.0 - alpha[..., None]))
            gray = cv2.cvtColor(composed.astype(np.uint8), cv2.COLOR_BGR2GRAY)
        else:
            gray = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY)
        return cv2.GaussianBlur(gray, (3, 3), 0)

    def _edge_map(self, gray):
        if gray is None:
            return None
        return cv2.Canny(gray, 32, 96)

    def _identify(self, hcursor):
        import win32ui
        try:
            # 捕获 48x48 区域，留出缩放空间
            width, height = 48, 48
            hdc = win32gui.GetDC(0)
            dc = win32ui.CreateDCFromHandle(hdc)
            memdc = dc.CreateCompatibleDC()
            bitmap = win32ui.CreateBitmap()
            bitmap.CreateCompatibleBitmap(dc, width, height)
            memdc.SelectObject(bitmap)
            win32gui.DrawIconEx(memdc.GetSafeHdc(), 0, 0, hcursor, width, height, 0, 0, win32con.DI_NORMAL)
            bits = bitmap.GetBitmapBits(True)
            target = np.frombuffer(bits, dtype='uint8').reshape(height, width, 4)
            memdc.DeleteDC()
            win32gui.ReleaseDC(0, hdc)

            best_name = 'Other'
            max_score = 0.0
            
            # 多尺度匹配：尝试 0.8, 1.0, 1.2 倍缩放，解决 UI 缩放问题
            best_scale = 1.0
            target_gray = self._normalize_for_match(target)
            target_edge = self._edge_map(target_gray)
            target_has_edge = target_edge is not None and np.count_nonzero(target_edge) > 0
            scales = [0.55, 0.65, 0.75, 0.85, 0.95, 1.0, 1.1, 1.2, 1.35, 1.5]
            for name, temp in self.templates.items():
                t_h, t_w = temp.shape[:2]
                for s in scales:
                    s_w, s_h = int(t_w * s), int(t_h * s)
                    if s_w > width or s_h > height:
                        continue

                    interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC
                    res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp)
                    temp_gray = self._normalize_for_match(res_temp)
                    if temp_gray is None:
                        continue

                    gray_res = cv2.matchTemplate(target_gray, temp_gray, cv2.TM_CCOEFF_NORMED)
                    _, gray_score, _, _ = cv2.minMaxLoc(gray_res)
                    score = gray_score

                    temp_edge = self._edge_map(temp_gray)
                    if target_has_edge and temp_edge is not None and np.count_nonzero(temp_edge) > 0:
                        edge_res = cv2.matchTemplate(target_edge, temp_edge, cv2.TM_CCOEFF_NORMED)
                        _, edge_score, _, _ = cv2.minMaxLoc(edge_res)
                        score = max(score, edge_score)
                    if score > max_score:
                        max_score = score
                        best_name = name
                        best_scale = s
            
            if max_score > 0.2:
                self._debug_log(f"best={best_name} score={max_score:.3f} scale={best_scale:.2f}")
                print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}")
            return best_name, max_score
        except Exception as e:
            return 'Other', 0.0


class AutoBotMove:
    def __init__(
        self,
        waypoints=None,
        waypoints_path=None,
        prepare_route_path=None,
        vendor_path=None,
        attack_loop_path=None,
        skinning_wait_sec=None,
        food_key=None,
        eat_hp_threshold=None,
        eat_max_wait_sec=None,
        stop_check=None,
        resurrection_waypoints_path=None,
        resurrection_route_a_path=None,
        resurrection_route_b_path=None,
        release_spirit_key=None,
        resurrect_key=None,
        enable_mouse_loot=True,
        turn_error_key=None,
        turn_error_hold_sec=None,
        distance_interact_pause_sec=None,
        mailbox_route_path=None,
    ):
        self.last_tab_time = 0
        self.last_interaction_time = 0  # 记录上一次按互动键的时间
        self.last_target_hp = 0         # 记录上一次的目标血量
        self._has_braked_for_target = False # 是否已经执行过刹车
        self.is_running = True
        self.is_moving = False
         # 记录上一帧是否处于战斗/有目标，用于检测“刚刚脱战”的瞬间
        self._was_in_combat_or_target = False
        self.skinning_wait_sec = float(skinning_wait_sec) if skinning_wait_sec is not None else 1.5
        self.enable_mouse_loot = enable_mouse_loot
        self.cursor_mgr = CursorManager()
        self.food_key = (food_key or DEFAULT_FOOD_KEY).strip().lower() or DEFAULT_FOOD_KEY
        self.eat_hp_threshold = int(eat_hp_threshold) if eat_hp_threshold is not None else DEFAULT_EAT_HP_THRESHOLD
        self.eat_max_wait_sec = float(eat_max_wait_sec) if eat_max_wait_sec is not None else DEFAULT_EAT_MAX_WAIT_SEC
        self.attack_loop_config = load_attack_loop(attack_loop_path)
        self._prev_death_state = 0
        self._eating_started_at = None
        self.last_turn_signal_time = 0.0
        self.combat_signal_retry_sec = 1.0
        self.turn_error_key = (turn_error_key or 's').strip().lower() or 's'
        self.turn_error_hold_sec = float(turn_error_hold_sec) if turn_error_hold_sec is not None else 0.8
        self.distance_interact_pause_until = 0.0
        self.distance_interact_brake_pending = False
        self.distance_interact_pause_sec = (
            float(distance_interact_pause_sec) if distance_interact_pause_sec is not None else 1.0
        )
        self.distance_interact_brake_sec = 0.08
        self._last_mouse_path_scale_signature = None
        self.logistics_resume_cooldown_until = 0.0
        # stop_check: 返回 True 表示需要立即停止（用于中断阻塞中的后勤/路线导航）
        self._stop_check = stop_check if callable(stop_check) else (lambda: False)
        if waypoints is None:
            path = waypoints_path or get_config_path(WAYPOINTS_FILE)
            waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS
        self.prepare_route_waypoints = []
        if prepare_route_path:
            try:
                self.prepare_route_waypoints = [
                    (float(point[0]), float(point[1]))
                    for point in (load_waypoints(prepare_route_path) or [])
                ]
            except Exception:
                self.prepare_route_waypoints = []
                print(f">>> [准备路线] 加载失败，忽略配置: {prepare_route_path}")
        self.prepare_route_index = 0
        self.prepare_route_completed = not bool(self.prepare_route_waypoints)
        self.is_running_prepare_route = bool(self.prepare_route_waypoints)
        self.prepare_route_snapped = False
        self.prepare_route_snap_max_dist = 10.0
        layout = load_layout_config()
        self.patrol_controller = CoordinatePatrol(
            waypoints,
            mount_key=str(layout.get("mount_key", "x") or "x"),
            mount_hold_sec=float(layout.get("mount_hold_sec", 1.6)),
            mount_retry_after_sec=float(layout.get("mount_retry_after_sec", 2.0)),
            enable_mount=bool(layout.get("enable_mount", True)),
        )
        self.death_manager = DeathManager(
            self.patrol_controller,
            resurrection_waypoints_path=resurrection_waypoints_path,
            resurrection_route_a_path=resurrection_route_a_path,
            resurrection_route_b_path=resurrection_route_b_path,
            release_spirit_key=layout.get('release_spirit_key', '9') if release_spirit_key is None else release_spirit_key,
            resurrect_key=layout.get('resurrect_key', '0') if resurrect_key is None else resurrect_key,
        )
        vendor_file = vendor_path or get_config_path('vendor.json')
        self.logistics_manager = LogisticsManager(vendor_file)
        self.logistics_manager.bag_full_hearthstone = bool(layout.get("bag_full_hearthstone", False))
        self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b")
        self.logistics_manager.hearthstone_cast_sec = float(layout.get("hearthstone_wait_sec", 12.0))
        self.logistics_manager.logistics_full_auto = bool(layout.get("logistics_full_auto", False))
        self.logistics_manager.bag_slot_threshold = int(layout.get("bag_slot_threshold", 2))
        self.logistics_manager.durability_threshold = float(layout.get("durability_threshold", 0.2))
        self.logistics_manager.repair_target_key = str(layout.get("repair_target_key", "8") or "8")
        self.logistics_manager.repair_interact_key = str(layout.get("repair_interact_key", "4") or "4")
        self.logistics_manager.enable_bag_full_mail = bool(layout.get("enable_bag_full_mail", False))
        self.logistics_manager.mailbox_route_file = str(
            mailbox_route_path
            or layout.get("mailbox_route_json", os.path.join("recorder", "mailbox.json"))
            or os.path.join("recorder", "mailbox.json")
        )
        self.logistics_manager.mailbox_interact_key = str(layout.get("mailbox_interact_key", "8") or "8")
        self.logistics_manager.mail_recipient_key = str(layout.get("mail_recipient_key", "") or "")
        self.logistics_manager.mail_send_key = str(layout.get("mail_send_key", "f8") or "f8")
        self.logistics_manager.mailbox_open_wait_sec = float(layout.get("mailbox_open_wait_sec", 2.0))
        self.logistics_manager.mail_send_wait_sec = float(layout.get("mail_send_wait_sec", 60.0))

    def _has_prepare_route(self) -> bool:
        return bool(self.prepare_route_waypoints)

    def _reset_prepare_route_for_logistics_resume(self):
        self.prepare_route_index = 0
        self.prepare_route_completed = not bool(self.prepare_route_waypoints)
        self.is_running_prepare_route = bool(self.prepare_route_waypoints)
        self.prepare_route_snapped = False
        self.last_turn_signal_time = 0.0
        self.last_interaction_time = 0.0
        self.distance_interact_brake_pending = False
        self.distance_interact_pause_until = 0.0
        self.logistics_resume_cooldown_until = time.time() + 5.0
        self.last_tab_time = time.time() + 1.0
        self.patrol_controller.stop_all()
        self.patrol_controller.reset_stuck()
        if self.prepare_route_waypoints:
            print(">>> [后勤] 全自动续跑：重置准备路线，准备返回巡逻。")
        else:
            print(">>> [后勤] 全自动续跑：未配置准备路线，直接恢复巡逻。")

    def _snap_prepare_route_to_nearest(self, state):
        if self.prepare_route_snapped or self.prepare_route_completed or not self._has_prepare_route():
            return
        self.prepare_route_snapped = True

        try:
            current_pos = (float(state.get('x')), float(state.get('y')))
        except Exception:
            return

        best_idx = None
        best_dist = None
        for idx, point in enumerate(self.prepare_route_waypoints):
            try:
                target = (float(point[0]), float(point[1]))
            except Exception:
                continue
            dist = self.patrol_controller.get_distance(current_pos, target)
            if best_dist is None or dist < best_dist:
                best_idx = idx
                best_dist = dist

        if best_idx is None or best_dist is None:
            return
        if best_dist > self.prepare_route_snap_max_dist:
            print(
                f">>> [准备路线] 离准备路线最近点仍较远（{best_dist:.2f}），"
                "从第 1 个点开始"
            )
            return

        self.prepare_route_index = best_idx
        self.patrol_controller.reset_stuck()
        print(
            f">>> [准备路线] 智能接入最近点 {best_idx + 1}/"
            f"{len(self.prepare_route_waypoints)}，距离 {best_dist:.2f}"
        )

    def _run_prepare_route_step(self, state):
        if self.prepare_route_completed or not self._has_prepare_route():
            self.prepare_route_completed = True
            self.is_running_prepare_route = False
            return

        self._snap_prepare_route_to_nearest(state)

        if self.prepare_route_index >= len(self.prepare_route_waypoints):
            self.prepare_route_completed = True
            self.is_running_prepare_route = False
            self.is_moving = False
            self.patrol_controller.stop_all()
            self.patrol_controller.reset_stuck()
            print(">>> [准备路线] 准备路线完成，开始正式巡逻...")
            return

        self.is_running_prepare_route = True
        target_pos = self.prepare_route_waypoints[self.prepare_route_index]
        is_arrived = self.patrol_controller.navigate_to_point(
            state,
            target_pos,
            stop_on_arrival=False,
        )
        self.is_moving = True
        if not is_arrived:
            return

        print(
            f">>> [准备路线] 到达准备路线点 {self.prepare_route_index + 1}/"
            f"{len(self.prepare_route_waypoints)}: {target_pos}"
        )
        self.prepare_route_index += 1
        if self.prepare_route_index >= len(self.prepare_route_waypoints):
            self.prepare_route_completed = True
            self.is_running_prepare_route = False
            self.is_moving = False
            self.patrol_controller.stop_all()
            self.patrol_controller.reset_stuck()
            print(">>> [准备路线] 准备路线完成，开始正式巡逻...")

    def _is_effective_target(self, state) -> bool:
        """
        判断当前 `state['target']` 是否是“可攻击的有效目标”。
        主要用 `target` 信号 + `target_hp` 是否为合理值，过滤掉 OCR/像素误判导致的假目标。
        """
        if not state.get('target'):
            return False
        target_hp = state.get('target_hp')
        if target_hp is None:
            return False
        try:
            target_hp = int(target_hp)
        except (TypeError, ValueError):
            return False
        # 目标血量为 0 基本可以视为无效/已死/不可交互目标
        if target_hp <= 0:
            return False
        return True

    def _should_stop(self) -> bool:
        try:
            return bool(self._stop_check())
        except Exception:
            return False

    def execute_disengage_loot(self):
        """从有战斗/目标切换到完全脱战的瞬间，执行拾取 + 剥皮。"""
        try:
            # 1. 拟人化鼠标扫雷补漏拾取（根据开关决定是否执行）
            if self.enable_mouse_loot:
                self.mouse_sweep_loot()
            else:
                # 关闭扫雷时，执行基础的交互拾取
                hw_ctrl.press(KEY_LOOT)
                time.sleep(0.8)
                # 关闭扫雷时，保留原有的补按交互和固定等待
                hw_ctrl.press(KEY_LOOT)
                time.sleep(self.skinning_wait_sec + 0.5)
        except Exception:
            pass

    def _load_mouse_path_points(self, client_width, client_height):
        path_points = []
        layout = load_layout_config()
        base_width = int(layout.get('mouse_path_base_window_width', 2560) or 2560)
        base_height = int(layout.get('mouse_path_base_window_height', 1600) or 1600)
        path_file = get_config_path("loot_path.json")
        if os.path.exists(path_file):
            with open(path_file, 'r', encoding='utf-8') as f:
                raw_path = json.load(f)

            if isinstance(raw_path, dict):
                path_points = raw_path.get('points') or raw_path.get('path_points') or raw_path.get('offsets') or []
                base_width = int(
                    raw_path.get('base_window_width')
                    or raw_path.get('window_width')
                    or raw_path.get('base_client_width')
                    or base_width
                )
                base_height = int(
                    raw_path.get('base_window_height')
                    or raw_path.get('window_height')
                    or raw_path.get('base_client_height')
                    or base_height
                )
            else:
                path_points = raw_path

            if path_points:
                scale_x = client_width / max(base_width, 1)
                scale_y = client_height / max(base_height, 1)
                signature = (client_width, client_height, base_width, base_height)
                if signature != self._last_mouse_path_scale_signature:
                    print(
                        f">>> [扫雷路径] 当前窗口 {client_width}x{client_height}，"
                        f"基准 {base_width}x{base_height}，缩放 x={scale_x:.3f} y={scale_y:.3f}"
                    )
                    self._last_mouse_path_scale_signature = signature

                scaled_points = []
                for point in path_points:
                    if not isinstance(point, (list, tuple)) or len(point) < 2:
                        continue
                    dx = int(round(float(point[0]) * scale_x))
                    dy = int(round(float(point[1]) * scale_y))
                    scaled_points.append((dx, dy))
                if scaled_points:
                    return scaled_points

        x_scale, y_scale = 1.8, 0.8
        for r in range(50, client_height // 2, 40):
            angles = range(180, 360, 5) if (r // 40) % 2 == 0 else range(360, 180, -5)
            for a in angles:
                rad = math.radians(a)
                path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale)))
        return path_points

    def mouse_sweep_scan(
        self,
        target_cursor_types,
        click_wait_map=None,
        max_scan_sec=15.0,
        score_threshold=0.7,
        return_on_first_click=False,
    ):
        hwnd = win32gui.FindWindow(None, WIN_TITLE)
        if not hwnd:
            return False

        click_wait_map = click_wait_map or {}
        target_cursor_types = set(target_cursor_types or [])

        try:
            left, top, right, bottom = get_wow_client_rect(hwnd)
            client_width = max(right - left, 1)
            client_height = max(bottom - top, 1)
            center_x = left + (right - left) // 2
            center_y = top + (bottom - top) // 2

            move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
            _, default_hcursor, _ = win32gui.GetCursorInfo()

            path_points = self._load_mouse_path_points(client_width, client_height)
            start_time = time.time()
            clicked_positions = []

            for dx, dy in path_points:
                if time.time() - start_time > max_scan_sec:
                    break

                target_x = center_x + dx + random.randint(-5, 5)
                target_y = center_y + dy + random.randint(-5, 5)

                if not (left + 10 < target_x < right - 10 and top + 10 < target_y < bottom - 10):
                    continue
                if any(math.dist((target_x, target_y), pos) < 30 for pos in clicked_positions):
                    continue

                move_cursor_hw(target_x, target_y, settle_sec=0.02)

                _, hcursor, _ = win32gui.GetCursorInfo()
                if hcursor == 0 or hcursor == default_hcursor:
                    if self._should_stop():
                        break
                    continue

                ctype_name, score = self.cursor_mgr.get_type(hcursor)
                if score > score_threshold and ctype_name in target_cursor_types:
                    print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击")
                    hw_ctrl.right_click()
                    clicked_positions.append((target_x, target_y))

                    wait_sec = float(click_wait_map.get(ctype_name, 0.3))
                    if wait_sec > 0:
                        time.sleep(wait_sec)

                    if return_on_first_click:
                        move_cursor_hw(center_x, center_y, settle_sec=0.02)
                        return True

                    wait_start = time.time()
                    while time.time() - wait_start < 0.8:
                        _, curr_h, _ = win32gui.GetCursorInfo()
                        check_name, _ = self.cursor_mgr.get_type(curr_h)
                        if check_name == 'Point':
                            break
                        time.sleep(0.1)
                    time.sleep(random.uniform(0.1, 0.2))
                elif score > 0.4:
                    print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 阈值 {score_threshold})")

                if self._should_stop():
                    break

            move_cursor_hw(center_x, center_y, settle_sec=0.02)
            return bool(clicked_positions)
        except Exception as e:
            print(f">>> [扫雷扫描] 出错: {e}")
            return False

    def mouse_scan_attack_target(self):
        return self.mouse_sweep_scan(
            ['Attack'],
            click_wait_map={'Attack': 0.3},
            max_scan_sec=4.0,
            score_threshold=0.6,
            return_on_first_click=True,
        )

    def mouse_sweep_loot(self):
        """支持图标识别的高精度扫雷拾取。"""
        return self.mouse_sweep_scan(
            ['LootAll', 'Skin'],
            click_wait_map={'LootAll': 1.3, 'Skin': self.skinning_wait_sec},
            max_scan_sec=15.0,
            score_threshold=0.7,
            return_on_first_click=False,
        )
        hwnd = win32gui.FindWindow(None, WIN_TITLE)
        if not hwnd: return False
        
        try:
            rect = win32gui.GetWindowRect(hwnd)
            left, top, right, bottom = rect
            center_x = left + (right - left) // 2
            center_y = top + (bottom - top) // 2
            
            # 1. 强制“角落校准”采样
            move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
            _, default_hcursor, _ = win32gui.GetCursorInfo()
            
            # 2. 获取扫瞄路径点位
            path_points = []
            path_file = get_config_path("loot_path.json")
            if os.path.exists(path_file):
                with open(path_file, 'r') as f:
                    path_points = json.load(f)
            else:
                # 降级方案：生成半椭圆点位
                x_scale, y_scale = 1.8, 0.8
                for r in range(50, (bottom-top)//2, 40):
                    angles = range(180, 360, 5) if (r//40)%2==0 else range(360, 180, -5)
                    for a in angles:
                        rad = math.radians(a)
                        path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale)))

            # 3. 开始沿路径扫瞄
            start_time = time.time()
            looted_positions = []
            
            for dx, dy in path_points:
                if time.time() - start_time > 15.0: break
                
                target_x = center_x + dx + random.randint(-5, 5)
                target_y = center_y + dy + random.randint(-5, 5)
                
                if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue
                if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue

                move_cursor_hw(target_x, target_y, settle_sec=0.02)
                
                _, hcursor, _ = win32gui.GetCursorInfo()
                if hcursor != 0 and hcursor != default_hcursor:
                    # 识别图标类型
                    ctype_name, score = self.cursor_mgr.get_type(hcursor)
                    
                    if score > 0.7 and ctype_name in ['LootAll', 'Skin']:
                        print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击")
                        hw_ctrl.right_click()
                        looted_positions.append((target_x, target_y))
                        
                        # 根据图标类型决定等待时间
                        wait_sec = 1.3 if ctype_name == 'LootAll' else self.skinning_wait_sec
                        time.sleep(wait_sec)
                        
                        # 等待指针恢复
                        wait_start = time.time()
                        while time.time() - wait_start < 0.8:
                            _, curr_h, _ = win32gui.GetCursorInfo()
                            check_name, _ = self.cursor_mgr.get_type(curr_h)
                            if check_name == 'Point': break
                            time.sleep(0.1)
                        time.sleep(random.uniform(0.1, 0.2))
                    elif score > 0.4:
                        # 记录一下高分但未达到门槛的识别，方便调试
                        print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 门槛 0.7)")

                if self._should_stop(): break
                    
            move_cursor_hw(center_x, center_y, settle_sec=0.02)
            return True
        except Exception as e:
            print(f">>> [扫雷拾取] 出错: {e}")
            return False

    def execute_combat_logic(self, state):
        if self.attack_loop_config:
            cfg = self.attack_loop_config
            if random.random() >= cfg['trigger_chance']:
                return
            hp = state.get('hp')
            if hp is not None and cfg.get('hp_below_percent') and cfg.get('hp_key') and hp < cfg['hp_below_percent']:
                hw_ctrl.press(cfg['hp_key'])
                time.sleep(0.2)
            mp = state.get('mp')
            if mp is not None and cfg.get('mp_below_percent') and cfg.get('mp_key') and mp < cfg['mp_below_percent']:
                hw_ctrl.press(cfg['mp_key'])
                time.sleep(0.2)
            
            # 每次攻击前选中目标
            # hw_ctrl.press(KEY_LOOT)
            for step in cfg['steps']:
                key = step.get('key') or KEY_ATTACK
                delay = float(step.get('delay', 0.5))
                hw_ctrl.press(key)
                time.sleep(delay)
            return
        # 默认：模拟手动按键的节奏
        if random.random() < 0.3:
            hw_ctrl.press(KEY_ATTACK)

    def _start_eating(self):
        """开始就地吃面包恢复。"""
        self._eating_started_at = time.time()
        hw_ctrl.press(self.food_key)

    def _should_keep_eating(self, state) -> bool:
        """
        返回 True 表示继续等待回血（暂停巡逻与找怪）；
        返回 False 表示结束进食等待。
        """
        if self._eating_started_at is None:
            return False
        hp = state.get('hp')
        if hp is not None and hp >= 100:
            self._eating_started_at = None
            return False
        if (time.time() - self._eating_started_at) >= self.eat_max_wait_sec:
            self._eating_started_at = None
            return False
        return True

    def execute_logic(self, state):
        if self._should_stop():
            # 在停止按钮点击后，确保马上松开移动键，并避免继续执行后勤交互。
            self.patrol_controller.stop_all()
            self.logistics_manager.is_returning = False
            self.is_moving = False
            return

        combat_error_signal = int(state.get('combat_error_signal', 255) or 255)
        death = state.get('death_state', 0)
        if self._prev_death_state in (1, 2) and death == 0:
            self.death_manager.reset_when_alive()
        self._prev_death_state = death
        # 1. 死亡状态：尸体(1) 记录坐标并释放灵魂；灵魂(2) 跑尸
        if death == 1:
            self.patrol_controller.stop_all()
            self.is_moving = False
            self.patrol_controller.reset_stuck()
            self.death_manager.on_death(state)
            return
        if death == 2:
            get_state = (lambda: None if self._should_stop() else parse_game_state())
            self.death_manager.run_to_corpse(state, get_state)
            return
        
        effective_target = self._is_effective_target(state)
        in_combat_or_target = bool(state['combat'] or effective_target)

        # 2. 后勤检查：只在脱战且无有效目标时触发，避免战斗中突然回城/修理/邮寄。
        if not in_combat_or_target and time.time() >= self.logistics_resume_cooldown_until:
            self.logistics_manager.check_logistics(state)
        else:
            self.logistics_manager.is_returning = False

        if self.logistics_manager.is_returning:
            if self.is_moving:
                self.patrol_controller.stop_all()
                self.is_moving = False
                self.patrol_controller.reset_stuck()

            tasks = self.logistics_manager.get_pending_tasks(state)
            get_state_fn = (lambda: None if self._should_stop() else parse_game_state())
            ok, completed_tasks = self.logistics_manager.run_pending_tasks(
                tasks,
                get_state_fn,
                self.patrol_controller,
                stop_check=self._should_stop,
            )

            can_resume = (
                ok
                and self.logistics_manager.logistics_full_auto
                and any(task in ("mail", "repair") for task in completed_tasks)
            )
            if can_resume:
                self._reset_prepare_route_for_logistics_resume()
                self.is_moving = False
                return

            if callable(getattr(self, '_on_hearthstone_stop', None)):
                self._on_hearthstone_stop()
            return

        # 3. 战斗/有目标：停止移动，执行攻击逻辑
        # 核心修改：只要还在战斗中，就不算“完全脱战”，即使当前没目标（可能正在 Tab 找下一个）
        
        if in_combat_or_target:
            # 被动进战时立即打断进食等待，转入正常战斗流程
            self._eating_started_at = None
            if self.is_moving:
                self.patrol_controller.stop_all()
                self.is_moving = False
                self.patrol_controller.reset_stuck()
            
            # 只有在有“活的目标”时才执行攻击和交互
            if effective_target:
                target_hp = state.get('target_hp', 0)
                if target_hp > 0:
                    now = time.time()
                    # 识别是否切换了新目标（血量跳变）
                    is_new_target = (target_hp > self.last_target_hp + 5)
                    if is_new_target:
                        self._has_braked_for_target = False
                        self.distance_interact_brake_pending = False
                        self.distance_interact_pause_until = 0.0
                    
                    # 1. 掉血刹车逻辑：第一次发现目标掉血且正在移动，按一下 S 停住
                    if not self._has_braked_for_target and 0 < target_hp < self.last_target_hp:
                        hw_ctrl.keyDown('s')
                        time.sleep(0.05)
                        hw_ctrl.keyUp('s')
                        self._has_braked_for_target = True

                    if self.distance_interact_brake_pending:
                        if now < self.distance_interact_pause_until:
                            self.last_target_hp = target_hp
                            self._was_in_combat_or_target = True
                            return
                        hw_ctrl.keyDown('s')
                        time.sleep(self.distance_interact_brake_sec)
                        hw_ctrl.keyUp('s')
                        self.distance_interact_brake_pending = False
                        self.distance_interact_pause_until = 0.0
                        self.last_target_hp = target_hp
                        self._was_in_combat_or_target = True
                        return
                    
                    # 根据战斗信号执行纠偏动作
                    if (
                        combat_error_signal == 100
                        and (now - self.last_interaction_time) >= self.combat_signal_retry_sec
                    ):
                        hw_ctrl.press(KEY_LOOT)
                        self.last_interaction_time = now
                        self.distance_interact_pause_until = now + self.distance_interact_pause_sec
                        self.distance_interact_brake_pending = True
                        self.last_target_hp = target_hp
                        self._was_in_combat_or_target = True
                        return

                    if (
                        combat_error_signal == 50
                        and (now - self.last_turn_signal_time) >= self.combat_signal_retry_sec
                    ):
                        hw_ctrl.keyDown(self.turn_error_key)
                        time.sleep(self.turn_error_hold_sec)
                        hw_ctrl.keyUp(self.turn_error_key)
                        self.last_turn_signal_time = now
                    
                    self.last_target_hp = target_hp
                    # 执行正常的攻击循环
                    self.execute_combat_logic(state)
            else:
                # 目标已死亡但还在战斗中，按 Tab 找下一个目标
                self.last_target_hp = 0
                self.last_turn_signal_time = 0.0
                self.last_interaction_time = 0.0
                self.distance_interact_brake_pending = False
                self.distance_interact_pause_until = 0.0
                self._has_braked_for_target = False
                if state['combat']:
                    hw_ctrl.press(KEY_TAB)
                    self.last_tab_time = time.time()
            
            self._was_in_combat_or_target = True
            return
        else:
            # 只有当 state['combat'] 和 effective_target 均为 False 时，才执行脱战逻辑
            if self._was_in_combat_or_target:
                # 此时已彻底脱战，执行拾取
                self.execute_disengage_loot()
                current_pos = None
                if state.get('x') is not None and state.get('y') is not None:
                    current_pos = (state.get('x'), state.get('y'))
                if current_pos is not None and self.prepare_route_completed:
                    self.patrol_controller.snap_to_forward_waypoint(current_pos)
                
                # 扫尾动作执行完后，本 tick 强制结束，防止立即按下 Tab
                self._was_in_combat_or_target = False
                self.last_turn_signal_time = 0.0
                self.last_interaction_time = 0.0
                self.distance_interact_brake_pending = False
                self.distance_interact_pause_until = 0.0
                self.last_tab_time = time.time() + 1.0 # 给找怪增加 1 秒额外冷却
                return 

            self.last_turn_signal_time = 0.0
            self.last_interaction_time = 0.0
            self.distance_interact_brake_pending = False
            self.distance_interact_pause_until = 0.0
            self._was_in_combat_or_target = False

            # 4. 脱战低血量：就地吃面包（最多等待 30 秒或回满）
            hp = state.get('hp')
            if self._eating_started_at is None and hp is not None and hp < self.eat_hp_threshold:
                if self.is_moving:
                    self.patrol_controller.stop_all()
                    self.is_moving = False
                self._start_eating()
            if self._should_keep_eating(state):
                # 进食期间不巡逻、不主动找目标（不按 Tab）
                return

            # 4. 没战斗没目标：优先执行准备路线，完成后再正式巡逻
            if not self.prepare_route_completed:
                self._run_prepare_route_step(state)
            else:
                self.is_moving = True
                self.patrol_controller.navigate(state)

        # 5. 顺便每隔几秒按一下 Tab（主动找怪）
        if self.prepare_route_completed and not effective_target and (time.time() - self.last_tab_time > 2.0):
            hw_ctrl.press(KEY_TAB)
            self.last_tab_time = time.time()

# 在 main 循环中使用：从 game_state 获取 state
if __name__ == "__main__":
    bot = AutoBotMove()
    try:
        while True:
            state = parse_game_state()
            if state:
                death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
                target_hp = state.get('target_hp')
                if target_hp is not None:
                    hp_part = f"血:{state['hp']}% 目标血:{target_hp}% 法:{state['mp']}%"
                else:
                    hp_part = f"血:{state['hp']}% 法:{state['mp']}%"
                print(
                    f"\r[状态] {hp_part} | "
                    f"战斗:{'Y' if state['combat'] else 'N'} 目标:{'Y' if state['target'] else 'N'} | "
                    f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
                    f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}°",
                    end=""
                )
                bot.execute_logic(state)
            time.sleep(0.1)
    except KeyboardInterrupt:
        print("\n已停止。")
