#!/usr/bin/env python3
"""
AutoPOE2 - Unified Live Sanity Probe Tool (Empirical Verification Engine)
========================================================================
Kiểm tra thực địa 5 trụ cột vật lý của bot trong < 1.5 giây:
1. --window : Trạng thái Process, HWND, Win32 ClientRect, DPI Scale, Focus Interlock & Black Screen test.
2. --vitals : Quét RPM trực tiếp vào RAM game thật, đối soát HP/Mana/XYZ và chống struct rác 778/779.
3. --shm    : Ping Shared Memory IPC (Local\\POE2_Auto_SharedMem_v1) sang C++ Core, đo RTT latency.
4. --log    : Đọc file Client.txt thật, kiểm tra Zone hiện tại, Area Generation seed và Loading screen.
5. --replay : Chạy kiểm chứng ngoại tuyến trên dữ liệu thật (captures/*.png & *_RAM.json) khi game đóng.
6. --all    : Chạy toàn bộ các bước trên và xuất bảng trạng thái tổng thể.

Tuân thủ nghiêm ngặt:
- Rule 8: Triệt tiêu sửa lỗi triệu chứng.
- Rule 9: Cổng kiểm thử đối chiếu độc lập (Verification Gate).
- Rule 14: 100% Authentic Real Data Protocol (Zero Mock Data).
"""

import argparse
import ctypes
from ctypes import wintypes
import json
import mmap
import os
import struct
import sys
import time
from typing import Any, Dict, List, Optional, Tuple

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")

# Thêm root vào sys.path
WORKSPACE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if WORKSPACE_ROOT not in sys.path:
    sys.path.insert(0, WORKSPACE_ROOT)

# ANSI Colors for Terminal Output
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"

SHARED_MEM_NAME = "Local\\POE2_Auto_SharedMem_v1"
SHARED_MEM_SIZE = 4 * 1024 * 1024
SHARED_MEM_MAGIC = 0x504F45324155544F


def print_banner():
    print(f"{CYAN}{BOLD}======================================================================{RESET}")
    print(f"{CYAN}{BOLD}       AutoPOE2 - Live Sanity Probe & Empirical Verification Gate     {RESET}")
    print(f"{CYAN}{BOLD}======================================================================{RESET}")


def get_poe2_pid() -> Optional[int]:
    """Tìm PID của tiến trình Path of Exile 2 (không dùng mock)."""
    try:
        import psutil
        for p in psutil.process_iter(["pid", "name"]):
            name = (p.info["name"] or "").lower()
            if "pathofexile" in name:
                return p.info["pid"]
    except Exception:
        pass

    # Fallback qua CreateToolhelp32Snapshot
    try:
        kernel32 = ctypes.windll.kernel32
        TH32CS_SNAPPROCESS = 0x00000002

        class PROCESSENTRY32W(ctypes.Structure):
            _fields_ = [
                ("dwSize", wintypes.DWORD),
                ("cntUsage", wintypes.DWORD),
                ("th32ProcessID", wintypes.DWORD),
                ("th32DefaultHeapID", ctypes.c_size_t),
                ("th32ModuleID", wintypes.DWORD),
                ("cntThreads", wintypes.DWORD),
                ("th32ParentProcessID", wintypes.DWORD),
                ("pcPriClassBase", wintypes.LONG),
                ("dwFlags", wintypes.DWORD),
                ("szExeFile", ctypes.c_wchar * 260),
            ]

        h_snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
        if h_snap and h_snap != -1:
            pe = PROCESSENTRY32W()
            pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
            if kernel32.Process32FirstW(h_snap, ctypes.byref(pe)):
                while True:
                    exe = pe.szExeFile.lower()
                    if "pathofexile" in exe:
                        kernel32.CloseHandle(h_snap)
                        return pe.th32ProcessID
                    if not kernel32.Process32NextW(h_snap, ctypes.byref(pe)):
                        break
            kernel32.CloseHandle(h_snap)
    except Exception:
        pass
    return None


def probe_window() -> Dict[str, Any]:
    """1. Kiểm tra cửa sổ game, DPI Scaling, Focus Interlock & Black Screen test."""
    t0 = time.perf_counter()
    user32 = ctypes.windll.user32
    gdi32 = ctypes.windll.gdi32

    pid = get_poe2_pid()
    result: Dict[str, Any] = {
        "probe": "WINDOW",
        "passed": False,
        "pid": pid,
        "hwnd": 0,
        "title": "",
        "rect": (0, 0, 0, 0),
        "dpi_scale": 1.0,
        "is_focused": False,
        "capture_status": "UNKNOWN",
        "duration_ms": 0.0,
    }

    if not pid:
        result["error"] = "Tiến trình PathOfExile.exe KHÔNG chạy."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    # Sử dụng ScreenCapturer để tìm HWND chuẩn xác kể cả khi chạy từ shell
    from src.assistant_tool.screen_capturer import ScreenCapturer, attach_to_default_desktop
    attach_to_default_desktop()
    capturer = ScreenCapturer()
    found_hwnd = capturer.find_poe2_window()

    if not found_hwnd:
        result["error"] = f"Tìm thấy PID {pid} nhưng KHÔNG tìm thấy HWND cửa sổ khả thị."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    result["hwnd"] = found_hwnd
    title_buf = (ctypes.c_wchar * 256)()
    user32.GetWindowTextW(found_hwnd, title_buf, 256)
    result["title"] = title_buf.value

    # Kích thước ClientRect
    rect = wintypes.RECT()
    user32.GetClientRect(found_hwnd, ctypes.byref(rect))
    w, h = rect.right - rect.left, rect.bottom - rect.top
    result["rect"] = (rect.left, rect.top, w, h)

    # DPI Scaling
    try:
        dpi = user32.GetDpiForWindow(found_hwnd)
        result["dpi_scale"] = round(dpi / 96.0, 2)
    except Exception:
        result["dpi_scale"] = 1.0

    # Focus Interlock
    fg_hwnd = user32.GetForegroundWindow()
    result["is_focused"] = (fg_hwnd == found_hwnd)

    # Thử chụp 1 vùng nhỏ 16x16 ở trung tâm xem có bị đen xì (Hardware Acceleration) không
    try:
        hdc_win = user32.GetDC(found_hwnd)
        hdc_mem = gdi32.CreateCompatibleDC(hdc_win)
        hbm = gdi32.CreateCompatibleBitmap(hdc_win, 16, 16)
        hbm_old = gdi32.SelectObject(hdc_mem, hbm)

        # Thử PrintWindow
        pw_ok = user32.PrintWindow(found_hwnd, hdc_mem, 2)  # PW_RENDERFULLCONTENT = 2
        if not pw_ok:
            # Fallback sang BitBlt
            gdi32.BitBlt(hdc_mem, 0, 0, 16, 16, hdc_win, w // 2, h // 2, 0x00CC0020)  # SRCCOPY

        # Kiểm tra pixel màu
        all_black = True
        for i in range(16):
            for j in range(16):
                px = gdi32.GetPixel(hdc_mem, i, j)
                if px != 0 and px != -1:
                    all_black = False
                    break
            if not all_black:
                break

        result["capture_status"] = "BLACK_SCREEN_WARNING" if all_black else "CAPTURED_OK"

        # Dọn dẹp GDI
        gdi32.SelectObject(hdc_mem, hbm_old)
        gdi32.DeleteObject(hbm)
        gdi32.DeleteDC(hdc_mem)
        user32.ReleaseDC(found_hwnd, hdc_win)
    except Exception as e:
        result["capture_status"] = f"GDI_ERROR: {e}"

    result["passed"] = (w >= 800 and h >= 600)
    result["duration_ms"] = (time.perf_counter() - t0) * 1000
    return result


def probe_vitals() -> Dict[str, Any]:
    """2. Quét RPM trực tiếp vào RAM game thật, kiểm tra HP/Mana/XYZ và chống struct rác 778/779."""
    t0 = time.perf_counter()
    pid = get_poe2_pid()
    result: Dict[str, Any] = {
        "probe": "VITALS_RPM",
        "passed": False,
        "pid": pid,
        "hp": (0, 0),
        "mana": (0, 0),
        "es": (0, 0),
        "xyz": (0.0, 0.0, 0.0),
        "has_778_bogus": False,
        "duration_ms": 0.0,
    }

    if not pid:
        result["error"] = "Game không chạy -> Bỏ qua Live RPM."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    try:
        from src.assistant_tool.sync_memory_snapshot import SyncMemorySnapshotManager
        mgr = SyncMemorySnapshotManager(pid=pid)
        snap = mgr.capture_ram_snapshot()
        if snap and "player" in snap:
            p = snap["player"]
            cur_hp = p.get("hp", 0)
            max_hp = p.get("max_hp", 0)
            cur_mana = p.get("mana", 0)
            max_mana = p.get("max_mana", 0)
            cur_es = p.get("es", 0)
            max_es = p.get("max_es", 0)
            pos_x = p.get("pos_x", 0.0)
            pos_y = p.get("pos_y", 0.0)
            pos_z = p.get("pos_z", 0.0)

            result["hp"] = (cur_hp, max_hp)
            result["mana"] = (cur_mana, max_mana)
            result["es"] = (cur_es, max_es)
            result["xyz"] = (round(pos_x, 1), round(pos_y, 1), round(pos_z, 1))

            if cur_hp == 778 and max_hp == 778:
                result["has_778_bogus"] = True
                result["error"] = "VI PHẠM: Dính mã Entity Marker rác 778/778 trong RAM!"
            elif max_hp > 0 and cur_hp <= max_hp:
                result["passed"] = True
            else:
                result["error"] = f"Chỉ số HP chưa hợp lệ (HP: {cur_hp}/{max_hp})"
        else:
            result["error"] = "Không thể trích xuất Snapshot Player từ RAM."
        mgr.close()
    except Exception as e:
        result["error"] = f"Lỗi RPM: {e}"

    result["duration_ms"] = (time.perf_counter() - t0) * 1000
    return result


def probe_shm() -> Dict[str, Any]:
    """3. Ping Shared Memory IPC (Local\\POE2_Auto_SharedMem_v1) sang C++ Core Engine."""
    t0 = time.perf_counter()
    result: Dict[str, Any] = {
        "probe": "SHM_IPC",
        "passed": False,
        "is_running": False,
        "magic_match": False,
        "protocol_version": 0,
        "telem_seq": 0,
        "cmd_seq": 0,
        "cmd_read": 0,
        "core_heartbeat_age_ms": -1,
        "duration_ms": 0.0,
    }

    try:
        shm = mmap.mmap(-1, SHARED_MEM_SIZE, tagname=SHARED_MEM_NAME, access=mmap.ACCESS_READ)
    except Exception:
        result["error"] = "Không mở được Shared Memory (AutoPOE2_Core.exe chưa khởi động)."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    try:
        header_raw = shm[:64]
        magic, proto, is_running, telem_seq, cmd_seq, cmd_read, core_hb, agent_hb, _ = struct.unpack(
            "<QIIQQQQQQ", header_raw
        )
        now_ms = int(time.time() * 1000)
        hb_age = now_ms - core_hb if core_hb > 0 else -1

        result["magic_match"] = (magic == SHARED_MEM_MAGIC)
        result["protocol_version"] = proto
        result["is_running"] = bool(is_running)
        result["telem_seq"] = telem_seq
        result["cmd_seq"] = cmd_seq
        result["cmd_read"] = cmd_read
        result["core_heartbeat_age_ms"] = hb_age

        if result["magic_match"] and hb_age >= 0 and hb_age < 3000:
            result["passed"] = True
        else:
            if not result["magic_match"]:
                result["error"] = f"Magic Header sai: 0x{magic:X} != 0x{SHARED_MEM_MAGIC:X}"
            elif hb_age >= 3000:
                result["error"] = f"Core Heartbeat bị kẹt / timeout ({hb_age}ms > 3000ms)"
            else:
                result["error"] = "Core chưa cập nhật Heartbeat"
    except Exception as e:
        result["error"] = f"Lỗi đọc SHM Header: {e}"
    finally:
        shm.close()

    result["duration_ms"] = (time.perf_counter() - t0) * 1000
    return result


def probe_log() -> Dict[str, Any]:
    """4. Đọc file Client.txt thật, kiểm tra Zone hiện tại, Area Generation seed và Loading screen."""
    t0 = time.perf_counter()
    from src.assistant_tool.config import Config
    from src.assistant_tool.log_watcher import LogWatcher

    log_path = Config.find_poe2_client_log()
    result: Dict[str, Any] = {
        "probe": "CLIENT_LOG",
        "passed": False,
        "log_path": log_path,
        "file_size_mb": 0.0,
        "last_modified_ago_sec": -1,
        "current_zone": "UNKNOWN",
        "last_seed": "UNKNOWN",
        "duration_ms": 0.0,
    }

    if not log_path or not os.path.exists(log_path):
        result["error"] = "Không tìm thấy tệp Client.txt hoặc LatestClient.txt hợp lệ."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    try:
        st = os.stat(log_path)
        result["file_size_mb"] = round(st.st_size / (1024 * 1024), 2)
        result["last_modified_ago_sec"] = int(time.time() - st.st_mtime)

        lw = LogWatcher(log_path)
        seeded = lw.seed_latest_playable_zone()
        if seeded:
            result["current_zone"] = seeded.area_name
            result["passed"] = True
        else:
            result["error"] = "Chưa nhận diện được Zone hợp lệ từ log."
    except Exception as e:
        result["error"] = f"Lỗi đọc Log: {e}"

    result["duration_ms"] = (time.perf_counter() - t0) * 1000
    return result


def probe_replay() -> Dict[str, Any]:
    """5. Chế độ kiểm chứng Replay ngoại tuyến trên dữ liệu thật (captures/) khi game tắt."""
    t0 = time.perf_counter()
    result: Dict[str, Any] = {
        "probe": "OFFLINE_REPLAY",
        "passed": False,
        "ram_artifact": "",
        "img_artifact": "",
        "invariant_check": "FAILED",
        "duration_ms": 0.0,
    }

    captures_dir = os.path.join(WORKSPACE_ROOT, "captures")
    if not os.path.exists(captures_dir):
        result["error"] = "Thư mục captures/ không tồn tại."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    # Tìm file *_RAM.json mới nhất
    ram_files = [os.path.join(captures_dir, f) for f in os.listdir(captures_dir) if f.endswith("_RAM.json")]
    if not ram_files:
        result["error"] = "Không tìm thấy tệp *_RAM.json nào trong captures/."
        result["duration_ms"] = (time.perf_counter() - t0) * 1000
        return result

    ram_files.sort(key=os.path.getmtime, reverse=True)
    latest_ram = ram_files[0]
    result["ram_artifact"] = os.path.basename(latest_ram)

    try:
        with open(latest_ram, "r", encoding="utf-8") as f:
            ram_data = json.load(f)

        from src.assistant_tool.agent_watcher import InvariantSentinel
        player_state = ram_data.get("player", {})
        errs = InvariantSentinel.check_vitals_invariants(player_state)
        if not errs:
            result["invariant_check"] = "ALL_INVARIANTS_PASSED"
            result["passed"] = True
        else:
            result["invariant_check"] = f"INVARIANT_VIOLATIONS: {errs}"
            result["error"] = f"Dữ liệu RAM thật vi phạm bất biến: {errs}"
    except Exception as e:
        result["error"] = f"Lỗi phân tích Replay: {e}"

    result["duration_ms"] = (time.perf_counter() - t0) * 1000
    return result


def run_all_probes():
    t_start = time.perf_counter()
    print_banner()

    p_win = probe_window()
    p_vit = probe_vitals()
    p_shm = probe_shm()
    p_log = probe_log()
    p_rep = probe_replay()

    total_duration_ms = (time.perf_counter() - t_start) * 1000

    print(f"\n{BOLD}1. WINDOW & RENDERING PROBE:{RESET}")
    if p_win["passed"]:
        focus_str = f"{GREEN}YES{RESET}" if p_win['is_focused'] else f"{YELLOW}NO (Unfocused){RESET}"
        print(f"   [PASS] HWND: 0x{p_win['hwnd']:X} | Title: '{p_win['title']}' | Rect: {p_win['rect'][2]}x{p_win['rect'][3]} | DPI: {p_win['dpi_scale']}x | Focus: {focus_str}")
        print(f"          Capture Engine: {GREEN}{p_win['capture_status']}{RESET} ({p_win['duration_ms']:.1f}ms)")
    else:
        print(f"   {YELLOW}[INFO] {p_win.get('error', 'Window not available')} ({p_win['duration_ms']:.1f}ms){RESET}")

    print(f"\n{BOLD}2. LIVE MEMORY (RPM) PROBE:{RESET}")
    if p_vit["passed"]:
        print(f"   [PASS] Player HP: {p_vit['hp'][0]}/{p_vit['hp'][1]} | Mana: {p_vit['mana'][0]}/{p_vit['mana'][1]} | ES: {p_vit['es'][0]}/{p_vit['es'][1]}")
        print(f"          Pos XYZ: {p_vit['xyz']} | Bogus 778 Check: {GREEN}CLEAN{RESET} ({p_vit['duration_ms']:.1f}ms)")
    else:
        print(f"   {YELLOW}[INFO] {p_vit.get('error', 'RPM inactive')} ({p_vit['duration_ms']:.1f}ms){RESET}")

    print(f"\n{BOLD}3. SHARED MEMORY IPC PROBE (C++ Core <-> Python):{RESET}")
    if p_shm["passed"]:
        print(f"   [PASS] Magic: OK | Version: {p_shm['protocol_version']} | Running: {p_shm['is_running']}")
        print(f"          Heartbeat Age: {p_shm['core_heartbeat_age_ms']}ms | TelemSeq: {p_shm['telem_seq']} | CmdQueue: {p_shm['cmd_seq']}/{p_shm['cmd_read']} ({p_shm['duration_ms']:.1f}ms)")
    else:
        print(f"   {YELLOW}[INFO] {p_shm.get('error', 'SHM inactive')} ({p_shm['duration_ms']:.1f}ms){RESET}")

    print(f"\n{BOLD}4. CLIENT.TXT LOG PROBE:{RESET}")
    if p_log["passed"]:
        print(f"   [PASS] File: {os.path.basename(p_log['log_path'])} ({p_log['file_size_mb']} MB, modified {p_log['last_modified_ago_sec']}s ago)")
        print(f"          Current Playable Zone: {GREEN}'{p_log['current_zone']}'{RESET} ({p_log['duration_ms']:.1f}ms)")
    else:
        print(f"   {YELLOW}[INFO] {p_log.get('error', 'Log inactive')} ({p_log['duration_ms']:.1f}ms){RESET}")

    print(f"\n{BOLD}5. OFFLINE AUTHENTIC REPLAY PROBE (Rule 14 Compliant):{RESET}")
    if p_rep["passed"]:
        print(f"   [PASS] Tested on real capture: {p_rep['ram_artifact']}")
        print(f"          Invariant Sentinel Status: {GREEN}{p_rep['invariant_check']}{RESET} ({p_rep['duration_ms']:.1f}ms)")
    else:
        print(f"   {RED}[FAIL] {p_rep.get('error', 'Replay failed')} ({p_rep['duration_ms']:.1f}ms){RESET}")

    print(f"\n{CYAN}----------------------------------------------------------------------{RESET}")
    print(f"{BOLD}Total Probe Execution Time: {GREEN if total_duration_ms < 1500 else YELLOW}{total_duration_ms:.1f} ms{RESET} (Target < 1500 ms)")
    print(f"{CYAN}======================================================================{RESET}\n")


def main():
    parser = argparse.ArgumentParser(description="AutoPOE2 Live Sanity Probe Tool")
    parser.add_argument("--window", action="store_true", help="Kiểm tra HWND, DPI, Focus và Screen Capture")
    parser.add_argument("--vitals", action="store_true", help="Quét RPM RAM game thật (HP/Mana/XYZ)")
    parser.add_argument("--shm", action="store_true", help="Ping Shared Memory IPC sang C++ Core")
    parser.add_argument("--log", action="store_true", help="Đọc Client.txt và kiểm tra Zone hiện tại")
    parser.add_argument("--replay", action="store_true", help="Kiểm chứng trên dữ liệu artifact thật (captures/)")
    parser.add_argument("--all", action="store_true", help="Chạy toàn bộ các probe (Mặc định)")

    args = parser.parse_args()

    if not any([args.window, args.vitals, args.shm, args.log, args.replay, args.all]):
        args.all = True

    if args.window:
        res = probe_window()
        print(json.dumps(res, indent=2, ensure_ascii=False))
    elif args.vitals:
        res = probe_vitals()
        print(json.dumps(res, indent=2, ensure_ascii=False))
    elif args.shm:
        res = probe_shm()
        print(json.dumps(res, indent=2, ensure_ascii=False))
    elif args.log:
        res = probe_log()
        print(json.dumps(res, indent=2, ensure_ascii=False))
    elif args.replay:
        res = probe_replay()
        print(json.dumps(res, indent=2, ensure_ascii=False))
    elif args.all:
        run_all_probes()


if __name__ == "__main__":
    main()
