#include "brain/map_device_handler.hpp"

#include <fstream>
#include <iostream>
#include <sstream>
#include <cmath>
#include <thread>
#include <chrono>
#include "common/logger.hpp"
#include "common/window_utils.hpp"
#include "common/toml_parser.hpp"
#include "common/math2d.hpp"
#include "navigation/pathfinder.hpp"
#include "navigation/terrain_grid.hpp"

namespace {

using common::ParseTomlFloat;
using common::ParseTomlInt;
using common::ParseTomlUint;
using common::ParseTomlBool;

static bool EnsureWindowFocus() {
    if (KMBoxNet::IsAnyIgnoreWindowFocus()) return true;
    HWND hwnd = common::FindPoe2Window();
    if (!hwnd) return false;
    return (GetForegroundWindow() == hwnd);
}

} // namespace

MapDeviceHandler::MapDeviceHandler() {
    LoadCoordinates("data/ui/coordinates.toml");
}

bool MapDeviceHandler::LoadCoordinates(const std::string& tomlPath) {
    std::ifstream file(tomlPath);
    if (!file.is_open()) {
        CoreLog("[MapDeviceHandler] Khong the mo file toa do: " + tomlPath + ". Dung toa do mac dinh 1080p.");
        return false;
    }

    std::string line;
    std::string currentSection;

    while (std::getline(file, line)) {
        // Bo qua comment va khoang trang
        size_t first = line.find_first_not_of(" \t\r\n");
        if (first == std::string::npos || line[first] == '#' || line[first] == ';') {
            continue;
        }

        // Section header
        if (line[first] == '[') {
            size_t last = line.find(']', first);
            if (last != std::string::npos) {
                currentSection = line.substr(first + 1, last - first - 1);
            }
            continue;
        }

        if (currentSection == "screen") {
            ParseTomlFloat(line, "reference_width", m_coords.refWidth);
            ParseTomlFloat(line, "reference_height", m_coords.refHeight);
        } else if (currentSection == "map_device") {
            ParseTomlFloat(line, "waystone_slot_x", m_coords.waystoneSlotX);
            ParseTomlFloat(line, "waystone_slot_y", m_coords.waystoneSlotY);
            ParseTomlFloat(line, "tablet_slot_x", m_coords.tabletSlotX);
            ParseTomlFloat(line, "tablet_slot_y", m_coords.tabletSlotY);
            ParseTomlFloat(line, "activate_button_x", m_coords.activateButtonX);
            ParseTomlFloat(line, "activate_button_y", m_coords.activateButtonY);
            ParseTomlFloat(line, "close_button_x", m_coords.closeButtonX);
            ParseTomlFloat(line, "close_button_y", m_coords.closeButtonY);
            ParseTomlUint(line, "waystone_row", m_coords.waystoneRow);
            ParseTomlUint(line, "waystone_col_start", m_coords.waystoneColStart);
            ParseTomlUint(line, "waystone_col_end", m_coords.waystoneColEnd);
            ParseTomlBool(line, "auto_scan_first_row", m_coords.autoScanFirstRow);
            ParseTomlUint(line, "scan_cell_delay_ms", m_coords.scanCellDelayMs);
            ParseTomlUint(line, "activate_delay_ms", m_coords.activateDelayMs);
            ParseTomlUint(line, "atlas_load_delay_ms", m_coords.atlasLoadDelayMs);
            ParseTomlFloat(line, "traverse_button_x", m_coords.traverseButtonX);
            ParseTomlFloat(line, "traverse_button_y", m_coords.traverseButtonY);
            ParseTomlFloat(line, "default_atlas_node_x", m_coords.defaultAtlasNodeX);
            ParseTomlFloat(line, "default_atlas_node_y", m_coords.defaultAtlasNodeY);
            ParseTomlFloat(line, "interact_radius", m_coords.interactRadius);
            ParseTomlBool(line, "require_ui_verification", m_coords.requireUiVerification);
            ParseTomlFloat(line, "shoreline_fallback_x", m_coords.shorelineFallbackX);
            ParseTomlFloat(line, "shoreline_fallback_y", m_coords.shorelineFallbackY);
            // TRAVERSE là nút kích hoạt map POE2; activate_button alias cùng tọa độ nếu file chưa có traverse_*
            if (line.find("activate_button_x") != std::string::npos) {
                m_coords.traverseButtonX = m_coords.activateButtonX;
            }
            if (line.find("activate_button_y") != std::string::npos) {
                m_coords.traverseButtonY = m_coords.activateButtonY;
            }
        } else if (currentSection == "inventory") {
            ParseTomlFloat(line, "origin_x", m_coords.invOriginX);
            ParseTomlFloat(line, "origin_y", m_coords.invOriginY);
            ParseTomlFloat(line, "cell_width", m_coords.invCellW);
            ParseTomlFloat(line, "cell_height", m_coords.invCellH);
            ParseTomlInt(line, "cols", m_coords.invCols);
            ParseTomlInt(line, "rows", m_coords.invRows);
        } else if (currentSection == "atlas_ui") {
            ParseTomlFloat(line, "viewport_center_x", m_coords.viewportCenterX);
            ParseTomlFloat(line, "viewport_center_y", m_coords.viewportCenterY);
            ParseTomlFloat(line, "search_box_x", m_coords.searchBoxX);
            ParseTomlFloat(line, "search_box_y", m_coords.searchBoxY);
            ParseTomlFloat(line, "zoom_in_button_x", m_coords.zoomInButtonX);
            ParseTomlFloat(line, "zoom_in_button_y", m_coords.zoomInButtonY);
            ParseTomlFloat(line, "zoom_out_button_x", m_coords.zoomOutButtonX);
            ParseTomlFloat(line, "zoom_out_button_y", m_coords.zoomOutButtonY);
            ParseTomlUint(line, "node_hover_delay_ms", m_coords.nodeHoverDelayMs);
            ParseTomlFloat(line, "precursor_tower_x", m_coords.precursorTowerX);
            ParseTomlFloat(line, "precursor_tower_y", m_coords.precursorTowerY);
            ParseTomlUint(line, "tablet_inventory_row", m_coords.tabletInventoryRow);
            ParseTomlFloat(line, "traverse_button_x", m_coords.traverseButtonX);
            ParseTomlFloat(line, "traverse_button_y", m_coords.traverseButtonY);
            ParseTomlFloat(line, "default_atlas_node_x", m_coords.defaultAtlasNodeX);
            ParseTomlFloat(line, "default_atlas_node_y", m_coords.defaultAtlasNodeY);
            ParseTomlUint(line, "atlas_load_delay_ms", m_coords.atlasLoadDelayMs);
        } else if (currentSection == "delays") {
            ParseTomlUint(line, "default_step_delay_ms", m_coords.stepDelayMs);
            ParseTomlUint(line, "jitter_ms", m_coords.jitterMs);
            ParseTomlUint(line, "activate_delay_ms", m_coords.activateDelayMs);
            ParseTomlUint(line, "atlas_load_delay_ms", m_coords.atlasLoadDelayMs);
        }
    }

    m_autoScanFirstRow = m_coords.autoScanFirstRow;

    CoreLog("[MapDeviceHandler] Da nap coordinates tu '" + tomlPath + "' (Ref: " +
            std::to_string(static_cast<int>(m_coords.refWidth)) + "x" +
            std::to_string(static_cast<int>(m_coords.refHeight)) +
            ", AutoScanRow0=" + (m_autoScanFirstRow ? "true" : "false") + ")");
    return true;
}

bool MapDeviceHandler::IsSafeTownZone(const TelemetryPacket& packet) const {
    // Kiem tra bat bien INV-TOWN-01: Tuyet doi khong duoc co quai vat song xung quanh
    for (uint32_t i = 0; i < packet.entityCount; ++i) {
        const auto& ent = packet.entities[i];
        if (ent.type == 1 && ent.maxHP > 0 && !(ent.extraFlags & 4)) {
            return false;
        }
    }
    return true;
}

bool MapDeviceHandler::StartMapDevice(const CommandPacket& cmd, uint64_t nowMs) {
    m_activeCmd = cmd;
    if (m_activeCmd.timeoutMs == 0) {
        m_activeCmd.timeoutMs = 45000;
    }
    m_routineStartMs = nowMs;
    m_lastStepMs = nowMs;
    m_state = MapDeviceState::ApproachDevice;
    m_interactFSent = false;
    m_atlasUiVerified = false;
    m_openDeviceRetryCount = 0;
    m_lastOpenClickMs = 0;
    m_firstOpenClickMs = 0;
    m_atlasOpenedMs = 0;
    m_hasPortalWorldPos = false;
    m_panelsClosed = false;
    m_heldApproachHids.clear();

    // cmd.targetX: cột cụ thể (0..11). Nếu > 0 hoặc không phải mặc định -> dùng cột đó
    // Nếu cmd.targetX <= 0: kích hoạt tự động quét hàng đầu tiên bắt đầu từ m_currentWaystoneCol
    if (cmd.targetX > 0.0f && cmd.targetX < static_cast<float>(m_coords.invCols)) {
        m_waystoneCol = static_cast<uint32_t>(cmd.targetX);
        m_waystoneRow = static_cast<uint32_t>(cmd.targetY);
        m_autoScanFirstRow = false;
    } else {
        m_waystoneRow = m_coords.waystoneRow;
        m_waystoneCol = m_currentWaystoneCol;
        m_autoScanFirstRow = m_coords.autoScanFirstRow;
    }
    m_useTablet = (cmd.targetZ >= 0.5f);

    if (!m_requiresNodeSelection) {
        m_requiresNodeSelection = true;
        m_targetNodeX = m_coords.defaultAtlasNodeX;
        m_targetNodeY = m_coords.defaultAtlasNodeY;
    }

    CoreLog("[MapDeviceHandler] Start MAP_DEVICE routine (Cmd #" + std::to_string(cmd.commandId) +
            ", Timeout=" + std::to_string(m_activeCmd.timeoutMs) +
            "ms, AutoScanRow0=" + (m_autoScanFirstRow ? "true" : "false") +
            ", StartCol=" + std::to_string(m_currentWaystoneCol) +
            ", DefaultNode=" + std::to_string(static_cast<int>(m_targetNodeX)) + "," +
            std::to_string(static_cast<int>(m_targetNodeY)) + ")");
    return true;
}

bool MapDeviceHandler::StartMapDevice(uint32_t waystoneCol, uint32_t waystoneRow, bool useTablet, uint64_t nowMs) {
    m_activeCmd = {};
    m_activeCmd.commandId = 700;
    m_activeCmd.timeoutMs = 45000;
    m_routineStartMs = nowMs;
    m_lastStepMs = nowMs;
    m_waystoneCol = waystoneCol;
    m_waystoneRow = waystoneRow;
    m_useTablet = useTablet;
    m_state = MapDeviceState::ApproachDevice;
    m_interactFSent = false;
    m_atlasUiVerified = false;
    m_openDeviceRetryCount = 0;
    m_lastOpenClickMs = 0;
    m_firstOpenClickMs = 0;
    m_atlasOpenedMs = 0;
    m_hasPortalWorldPos = false;
    m_panelsClosed = false;
    m_heldApproachHids.clear();

    if (waystoneCol > 0 || waystoneRow > 0) {
        m_autoScanFirstRow = false;
    } else {
        m_autoScanFirstRow = m_coords.autoScanFirstRow;
    }

    if (!m_requiresNodeSelection) {
        m_requiresNodeSelection = true;
        m_targetNodeX = m_coords.defaultAtlasNodeX;
        m_targetNodeY = m_coords.defaultAtlasNodeY;
    }

    CoreLog("[MapDeviceHandler] Start MAP_DEVICE routine (Waystone [" +
            std::to_string(waystoneCol) + "," + std::to_string(waystoneRow) + "], Tablet=" +
            (useTablet ? "true" : "false") + ", AutoScanRow0=" + (m_autoScanFirstRow ? "true" : "false") +
            ", StartCol=" + std::to_string(m_currentWaystoneCol) +
            ", DefaultNode=" + std::to_string(static_cast<int>(m_targetNodeX)) + "," +
            std::to_string(static_cast<int>(m_targetNodeY)) + ")");
    return true;
}

bool MapDeviceHandler::SelectAtlasNode(const CommandPacket& cmd, uint64_t nowMs) {
    m_activeCmd = cmd;
    m_routineStartMs = nowMs;
    m_lastStepMs = nowMs;
    m_targetNodeX = cmd.targetX;
    m_targetNodeY = cmd.targetY;
    m_nodeFlags = static_cast<uint32_t>(cmd.targetZ);
    m_targetNodeId = cmd.targetEntityId;
    m_requiresNodeSelection = (cmd.targetX > 0.0f && cmd.targetY > 0.0f);
    m_hoverStarted = false;

    CoreLog("[MapDeviceHandler] SelectAtlasNode received (X=" + std::to_string(m_targetNodeX) +
            ", Y=" + std::to_string(m_targetNodeY) + ", Flags=0x" +
            std::to_string(m_nodeFlags) + ", NodeId=" + std::to_string(m_targetNodeId) + ")");

    if (m_state == MapDeviceState::OpenDevice) {
        m_state = MapDeviceState::SelectAtlasNode;
    } else {
        m_state = MapDeviceState::ApproachDevice;
    }
    return true;
}

bool MapDeviceHandler::SelectAtlasNode(float screenX, float screenY, uint32_t flags, uint32_t nodeId, uint64_t nowMs) {
    CommandPacket cmd{};
    cmd.commandId = 901;
    cmd.opCode = MacroOpCode::SELECT_ATLAS_NODE;
    cmd.targetX = screenX;
    cmd.targetY = screenY;
    cmd.targetZ = static_cast<float>(flags);
    cmd.targetEntityId = nodeId;
    cmd.priority = 3200;
    cmd.timeoutMs = 5000;
    return SelectAtlasNode(cmd, nowMs);
}

void MapDeviceHandler::Reset() {
    if (m_isZoomedIn) {
        KMBoxNet kmbox;
        kmbox.EnableEmulationMode(true);
        kmbox.Wheel(-120 * static_cast<int>(m_zoomSteps));
        m_isZoomedIn = false;
    }
    m_state = MapDeviceState::Idle;
    m_activeCmd = {};
    m_routineStartMs = 0;
    m_lastStepMs = 0;
    m_targetDeviceEntityId = 0;
    m_targetPortalEntityId = 0;
    m_targetNodeX = 0.0f;
    m_targetNodeY = 0.0f;
    m_nodeFlags = 0;
    m_targetNodeId = 0;
    m_requiresNodeSelection = false;
    m_hoverStarted = false;
    m_deviceWorldX = 0.0f;
    m_deviceWorldY = 0.0f;
    m_deviceWorldZ = 0.0f;
    m_hasDeviceWorldPos = false;
    m_tangentHand = 0;
    m_lastPlayerX = 0.0f;
    m_lastPlayerY = 0.0f;
    m_lastMoveCheckMs = 0;
    m_lastTargetScreenX = 0.0f;
    m_lastTargetScreenY = 0.0f;
    m_heldApproachHids.clear();
    m_interactFSent = false;
    m_atlasUiVerified = false;
    m_openDeviceRetryCount = 0;
    m_lastOpenClickMs = 0;
    m_firstOpenClickMs = 0;
    m_atlasOpenedMs = 0;
    m_portalWorldX = 0.0f;
    m_portalWorldY = 0.0f;
    m_hasPortalWorldPos = false;
    m_panelsClosed = false;
}

ActionProposal MapDeviceHandler::Propose(const TelemetryPacket& packet, uint64_t nowMs) {
    if (!IsActive()) {
        return {};
    }

    // 1. Kiem tra bat bien INV-TOWN-01: An toan tai Town/Hideout
    if (!IsSafeTownZone(packet)) {
        CoreLog("[MapDeviceHandler] INVARIANT VIOLATION: Monster detected near Map Device! Aborting routine.");
        m_state = MapDeviceState::Failed;
        return {};
    }

    // 2. Kiem tra Timeout (chu trình Hideout→TRAVERSE cần ~45s, không còn 20s)
    const uint64_t timeout = m_activeCmd.timeoutMs > 0 ? m_activeCmd.timeoutMs : 45000;
    if (nowMs - m_routineStartMs > timeout) {
        CoreLog("[MapDeviceHandler] Routine timeout (" + std::to_string(timeout) + "ms) reached. Aborting.");
        m_state = MapDeviceState::Failed;
        return {};
    }

    // 3. De xuat hanh dong SWITCH_AREA tai Hideout voi priority 3200 (cao hon Town Explore 1000)
    ActionProposal p{};
    p.kind = BotActionKind::SwitchArea;
    p.priority = 3200;
    p.priorityName = "MAP_DEVICE";
    p.entityId = m_targetPortalEntityId > 0 ? m_targetPortalEntityId : m_targetDeviceEntityId;
    p.distance = 15.0f;
    return p;
}

bool MapDeviceHandler::Execute(const ActionProposal& chosen, const TelemetryPacket& packet, KMBoxNet& kmbox, uint64_t nowMs) {
    if (chosen.kind != BotActionKind::SwitchArea) return false;
    if (!EnsureWindowFocus()) return false;

    return AdvanceFsm(packet, kmbox, nowMs);
}

bool MapDeviceHandler::AdvanceFsm(const TelemetryPacket& packet, KMBoxNet& kmbox, uint64_t nowMs) {
    const uint32_t delay = m_hoverStarted ? m_coords.nodeHoverDelayMs : m_coords.stepDelayMs;
    const bool skipStepDelay =
        m_state == MapDeviceState::ApproachDevice ||
        m_state == MapDeviceState::OpenDevice ||
        m_state == MapDeviceState::WaitForPortals ||
        m_state == MapDeviceState::EnterPortal;
    if (!skipStepDelay && nowMs - m_lastStepMs < delay) {
        return false; // Cho delay tu nhien (UI hover / waystone)
    }

    int actualW = 0;
    int actualH = 0;
    common::GetGameResolution(actualW, actualH);
    const float screenW = (actualW > 0) ? static_cast<float>(actualW) : 1920.0f;
    const float screenH = (actualH > 0) ? static_cast<float>(actualH) : 1080.0f;

    switch (m_state) {
        case MapDeviceState::ApproachDevice: {
            // Tìm thực thể Map Device đích thực trong phạm vi (ưu tiên tên chứa "Map Device", "Device", "Atlas")
            int bestIdx = -1;
            float closestDist = 99999.0f;
            for (uint32_t i = 0; i < packet.entityCount; ++i) {
                const auto& ent = packet.entities[i];
                if ((ent.type == 3 || ent.type == 4) && !(ent.extraFlags & 4)) {
                    std::string lower(ent.name);
                    for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
                    if (lower.find("map device") != std::string::npos ||
                        lower.find("device") != std::string::npos ||
                        lower.find("atlas") != std::string::npos) {
                        float dx = ent.posX - packet.player.posX;
                        float dy = ent.posY - packet.player.posY;
                        float d = std::sqrt(dx * dx + dy * dy);
                        if (d < closestDist) {
                            closestDist = d;
                            bestIdx = static_cast<int>(i);
                        }
                    }
                }
            }
            if (bestIdx >= 0) {
                const auto& ent = packet.entities[bestIdx];
                m_targetDeviceEntityId = ent.id;
                if (std::fabs(ent.posX) > 0.001f || std::fabs(ent.posY) > 0.001f || std::fabs(ent.posZ) > 0.001f) {
                    m_deviceWorldX = ent.posX;
                    m_deviceWorldY = ent.posY;
                    m_deviceWorldZ = ent.posZ;
                    m_hasDeviceWorldPos = true;
                }
            }

            // Kiểm tra cự ly tới Map Device (INV-NAV-TERRAIN-COMMERCIAL)
            float dx = m_hasDeviceWorldPos ? (m_deviceWorldX - packet.player.posX) : 0.0f;
            float dy = m_hasDeviceWorldPos ? (m_deviceWorldY - packet.player.posY) : 0.0f;
            float dist = std::sqrt(dx * dx + dy * dy);

            // Bất biến INV-NAV-TERRAIN-COMMERCIAL:
            // CẤM GIẢ ĐỊNH Map Device ở gần spawn. Nếu khoảng cách > interactRadius (18.0f), di chuyển tiếp cận!
            if (m_hasDeviceWorldPos && dist > m_coords.interactRadius) {
                // Kiểm tra va quệt chướng ngại vật / gờ đá (Delta XYZ ~ 0)
                if (m_lastMoveCheckMs > 0 && (nowMs - m_lastMoveCheckMs >= 350)) {
                    float moveDx = packet.player.posX - m_lastPlayerX;
                    float moveDy = packet.player.posY - m_lastPlayerY;
                    float moveDistSq = moveDx * moveDx + moveDy * moveDy;
                    if (moveDistSq < 1.5f) {
                        // Va chạm bờ tường hoặc gờ đá hideout -> Trượt tiếp tuyến!
                        navigation::Pathfinder pf;
                        auto slide = pf.ComputeTangentSlide(
                            navigation::Vec2(packet.player.posX, packet.player.posY),
                            navigation::Vec2(dx, dy),
                            navigation::Vec2(m_deviceWorldX, m_deviceWorldY),
                            m_terrainGrid,
                            m_tangentHand
                        );
                        m_tangentHand = slide.handDirection;
                        dx = slide.slideVector.x * 30.0f;
                        dy = slide.slideVector.y * 30.0f;
                        CoreLog("[INV-NAV-TERRAIN-COMMERCIAL] Map Device approach blocked -> Tangent sliding with terrain grid!");
                    } else {
                        m_tangentHand = 0;
                    }
                }
                m_lastPlayerX = packet.player.posX;
                m_lastPlayerY = packet.player.posY;
                m_lastMoveCheckMs = nowMs;

                // Chiếu hướng di chuyển sang WASD (hỗ trợ 8 hướng chéo đồng thời)
                // Tuân thủ INV-WASD-MIN-DWELL: Dwell tối thiểu 300ms, cấm micro-tap < 150ms
                const auto wasd = common::WorldToWasd(dx, dy);
                if (!wasd.empty()) {
                    std::vector<uint8_t> keys;
                    if (wasd.up) keys.push_back(0x1A);    // 'W'
                    if (wasd.down) keys.push_back(0x16);  // 'S'
                    if (wasd.left) keys.push_back(0x04);  // 'A'
                    if (wasd.right) keys.push_back(0x07); // 'D'
                    HoldApproachWasd(keys, kmbox);
                } else {
                    ReleaseApproachKeys(kmbox);
                }
                m_lastStepMs = nowMs;
                return true;
            }

            // Đã tiếp cận trong cự ly tương tác (hoặc dùng fallback vị trí Hideout) -> Chuyển sang OpenDevice
            ReleaseApproachKeys(kmbox);
            m_state = MapDeviceState::OpenDevice;
            m_interactFSent = false;
            m_atlasOpenedMs = 0;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] State: ApproachDevice -> OpenDevice (In interact radius: dist=" +
                    std::to_string(dist) + ", TargetId=" + std::to_string(m_targetDeviceEntityId) + ")");
            return true;
        }

        case MapDeviceState::OpenDevice: {
            ReleaseApproachKeys(kmbox);

            float targetScreenX = 0.0f;
            float targetScreenY = 0.0f;

            if (m_hasDeviceWorldPos) {
                float dx = m_deviceWorldX - packet.player.posX;
                float dy = m_deviceWorldY - packet.player.posY;
                float worldDist = std::sqrt(dx * dx + dy * dy);
                float pixelDist = (std::min)(worldDist * 16.0f, 450.0f);
                auto screenPt = common::WorldToScreenIsometric(dx, dy, pixelDist, screenW, screenH, 80.0f);
                targetScreenX = screenPt.x;
                targetScreenY = screenPt.y;
                CoreLog("[INV-NAV-TERRAIN-COMMERCIAL] Chieu 3D WorldToScreen Map Device: (" +
                        std::to_string(static_cast<int>(m_deviceWorldX)) + ", " +
                        std::to_string(static_cast<int>(m_deviceWorldY)) + ") -> Pixel (" +
                        std::to_string(static_cast<int>(targetScreenX)) + ", " +
                        std::to_string(static_cast<int>(targetScreenY)) + ")");
            } else {
                auto pt = m_coords.ScalePoint(m_coords.shorelineFallbackX, m_coords.shorelineFallbackY, screenW, screenH);
                targetScreenX = pt.x;
                targetScreenY = pt.y;
            }

            m_lastTargetScreenX = targetScreenX;
            m_lastTargetScreenY = targetScreenY;

            const bool shouldClick = !m_interactFSent ||
                (!m_atlasUiVerified && m_coords.requireUiVerification &&
                 m_openDeviceRetryCount < 2 &&
                 (nowMs - m_lastOpenClickMs >= m_coords.openDeviceRetryIntervalMs) &&
                 (nowMs - m_firstOpenClickMs < m_coords.openDeviceTimeoutMs));

            if (shouldClick) {
                // [INV-NAV-TERRAIN-COMMERCIAL] Dynamic Camera Zoom-in trước khi click
                if (!m_isZoomedIn) {
                    kmbox.Wheel(120 * static_cast<int>(m_zoomSteps));
                    m_isZoomedIn = true;
                    CoreLog("[INV-NAV-TERRAIN-COMMERCIAL] Dynamic Camera Zoom-in: Phong to dien tich click Map Device (+480 wheel)!");
                }

                kmbox.MoveMouseSmooth(static_cast<int>(targetScreenX), static_cast<int>(targetScreenY), m_curveGen, 12, 1);
                // Trong POE2 KBM, tương tác (Interact) mở Map Device là Click chuột trái (LMB) khi trỏ vào bệ đá
                int dwellMs = m_curveGen.GenerateDwellTimeMs(45.0f, 8.0f);
                kmbox.ClickMouse(1, dwellMs);

                if (!m_interactFSent) {
                    m_firstOpenClickMs = nowMs;
                }
                m_interactFSent = true;
                m_lastOpenClickMs = nowMs;
                m_atlasOpenedMs = nowMs;
                m_lastStepMs = nowMs;
                m_openDeviceRetryCount++;

                CoreLog("[MapDeviceHandler] OpenDevice: da click LMB len Map Device tai (" +
                        std::to_string(static_cast<int>(targetScreenX)) + "," +
                        std::to_string(static_cast<int>(targetScreenY)) + ") [Lan " +
                        std::to_string(m_openDeviceRetryCount) + "], cho UI Atlas xac nhan...");
                if (m_coords.requireUiVerification || m_coords.atlasLoadDelayMs > 0) {
                    return true;
                }
            }

            // [INV-MAPDEVICE-VERIFY-UI] Khóa chặn bắt buộc: Cấm advance nếu chưa có tín hiệu UI mở
            if (m_coords.requireUiVerification) {
                if (m_atlasUiVerified) {
                    CoreLog("[INV-MAPDEVICE-VERIFY-UI] Atlas UI DA DUOC XAC NHAN MO! Cho phep FSM tiep tuc.");
                    if (m_requiresNodeSelection) {
                        m_state = MapDeviceState::SelectAtlasNode;
                        CoreLog("[MapDeviceHandler] State: OpenDevice -> SelectAtlasNode");
                    } else {
                        m_state = MapDeviceState::PlaceWaystone;
                        CoreLog("[MapDeviceHandler] State: OpenDevice -> PlaceWaystone");
                    }
                    m_lastStepMs = nowMs;
                    return true;
                }

                if (nowMs - m_firstOpenClickMs < m_coords.openDeviceTimeoutMs) {
                    return false; // Tiep tuc cho xac thuc tu Coordinator / OCR
                }

                // Timeout ma UI van khong mo -> Fail-closed, bam Space thoat khoi menu kẹt
                CoreLog("[INV-MAPDEVICE-VERIFY-UI] LOI KHOA CHAN: Qua timeout " +
                        std::to_string(m_coords.openDeviceTimeoutMs) +
                        "ms nhung Atlas UI chua mo! Abort routine de tranh click mu tui do!");
                kmbox.KeyPress(0x2C, 40); // SPACE
                if (m_isZoomedIn) {
                    kmbox.Wheel(-120 * static_cast<int>(m_zoomSteps));
                    m_isZoomedIn = false;
                }
                m_state = MapDeviceState::Failed;
                m_lastStepMs = nowMs;
                return false;
            }

            // Che do khong yeu cau UI verification (unit tests / mock mode)
            if (m_coords.atlasLoadDelayMs > 0 && nowMs - m_atlasOpenedMs < m_coords.atlasLoadDelayMs) {
                return false;
            }

            if (m_requiresNodeSelection) {
                m_state = MapDeviceState::SelectAtlasNode;
                CoreLog("[MapDeviceHandler] State: OpenDevice -> SelectAtlasNode");
            } else {
                m_state = MapDeviceState::PlaceWaystone;
                CoreLog("[MapDeviceHandler] State: OpenDevice -> PlaceWaystone");
            }
            m_lastStepMs = nowMs;
            return true;
        }

        case MapDeviceState::SelectAtlasNode: {
            // [DOC-56] Định vị & Hover xác nhận Node bản đồ tối ưu trên cây Atlas (Non-blocking 120Hz Hot Path)
            float nodeX = (m_targetNodeX > 0.0f) ? m_targetNodeX : m_coords.viewportCenterX;
            float nodeY = (m_targetNodeY > 0.0f) ? m_targetNodeY : m_coords.viewportCenterY;
            auto nodePt = m_coords.ScalePoint(nodeX, nodeY, screenW, screenH);

            if (!m_hoverStarted) {
                // Giai đoạn 1: Di chuột đến vị trí Node bằng quỹ đạo Bézier mượt mà (INV-INPUT-ABS-MOUSE)
                kmbox.MoveMouseSmooth(static_cast<int>(nodePt.x), static_cast<int>(nodePt.y), m_curveGen, 12, 1);
                m_hoverStarted = true;
                m_lastStepMs = nowMs;
                CoreLog("[MapDeviceHandler] Hovering Atlas Node at (" +
                        std::to_string(static_cast<int>(nodeX)) + "," +
                        std::to_string(static_cast<int>(nodeY)) + ") Screen (" +
                        std::to_string(static_cast<int>(nodePt.x)) + "," +
                        std::to_string(static_cast<int>(nodePt.y)) + ")");
                return true;
            }

            // Giai đoạn 2: Click chọn Node sau khi hover delay hoàn tất (da vuot qua check delay o dau ham AdvanceFsm)
            kmbox.ClickMouse(1, 40);
            m_hoverStarted = false;

            CoreLog("[MapDeviceHandler] Selected Atlas Node confirmed at (" +
                    std::to_string(static_cast<int>(nodeX)) + "," +
                    std::to_string(static_cast<int>(nodeY)) + ") Screen (" +
                    std::to_string(static_cast<int>(nodePt.x)) + "," +
                    std::to_string(static_cast<int>(nodePt.y)) + ")");

            if (m_nodeFlags & ATLAS_FLAG_SOCKET_PRECURSOR_TOWER) {
                m_state = MapDeviceState::InspectPrecursorTowers;
                CoreLog("[MapDeviceHandler] State: SelectAtlasNode -> InspectPrecursorTowers");
            } else {
                m_state = MapDeviceState::PlaceWaystone;
                CoreLog("[MapDeviceHandler] State: SelectAtlasNode -> PlaceWaystone");
            }
            m_lastStepMs = nowMs;
            return true;
        }

        case MapDeviceState::InspectPrecursorTowers: {
            // [DOC-56] Click Tháp Dẫn Đường Precursor Tower và socket Tablet
            auto towerPt = m_coords.ScalePoint(m_coords.precursorTowerX, m_coords.precursorTowerY, screenW, screenH);
            kmbox.MoveMouseSmooth(static_cast<int>(towerPt.x), static_cast<int>(towerPt.y), m_curveGen, 12, 1);
            kmbox.ClickMouse(1, 40);

            // Nạp Tablet từ hàng tabletInventoryRow trong túi đồ (mặc định Row 1)
            float tabInvX = m_coords.invOriginX + m_coords.invCellW * 0.5f;
            float tabInvY = m_coords.invOriginY + static_cast<float>(m_coords.tabletInventoryRow) * m_coords.invCellH + m_coords.invCellH * 0.5f;
            auto tabInvPt = m_coords.ScalePoint(tabInvX, tabInvY, screenW, screenH);

            kmbox.KeyDown(VK_CONTROL);
            kmbox.MoveMouseSmooth(static_cast<int>(tabInvPt.x), static_cast<int>(tabInvPt.y), m_curveGen, 12, 1);
            kmbox.ClickMouse(1, 40);
            kmbox.KeyUp(VK_CONTROL);

            CoreLog("[MapDeviceHandler] Socketed Precursor Tablet from Row " +
                    std::to_string(m_coords.tabletInventoryRow));

            m_state = MapDeviceState::PlaceWaystone;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] State: InspectPrecursorTowers -> PlaceWaystone");
            return true;
        }

        case MapDeviceState::PlaceWaystone: {
            if (m_autoScanFirstRow) {
                // Tu dong quet toan bo hang dau tien (row = m_coords.waystoneRow, mac dinh 0)
                // Quet tuan tu tu m_currentWaystoneCol xoay vong qua cac o (cols waystoneColStart..waystoneColEnd)
                const uint32_t colStart = m_coords.waystoneColStart;
                const uint32_t colEnd = (m_coords.waystoneColEnd < static_cast<uint32_t>(m_coords.invCols))
                                        ? m_coords.waystoneColEnd
                                        : static_cast<uint32_t>(m_coords.invCols - 1);
                const uint32_t totalCols = (colEnd >= colStart) ? (colEnd - colStart + 1) : 1;

                kmbox.KeyDown(VK_CONTROL);
                for (uint32_t i = 0; i < totalCols; ++i) {
                    uint32_t c = colStart + ((m_currentWaystoneCol - colStart + i) % totalCols);
                    float invX = m_coords.invOriginX + static_cast<float>(c) * m_coords.invCellW + m_coords.invCellW * 0.5f;
                    float invY = m_coords.invOriginY + static_cast<float>(m_coords.waystoneRow) * m_coords.invCellH + m_coords.invCellH * 0.5f;
                    auto invPt = m_coords.ScalePoint(invX, invY, screenW, screenH);

                    kmbox.MoveMouseSmooth(static_cast<int>(invPt.x), static_cast<int>(invPt.y), m_curveGen, 8, 1);
                    kmbox.ClickMouse(1, 20);
                }
                kmbox.KeyUp(VK_CONTROL);

                // Tinh tien m_currentWaystoneCol cho lan chay tiep theo
                m_currentWaystoneCol = colStart + ((m_currentWaystoneCol - colStart + 1) % totalCols);
                CoreLog("[MapDeviceHandler] PlaceWaystone: Auto-scanned row " + std::to_string(m_coords.waystoneRow) +
                        " (cols " + std::to_string(colStart) + ".." + std::to_string(colEnd) +
                        "), next start col=" + std::to_string(m_currentWaystoneCol));
            } else {
                // Che do nham o don le co dinh
                float invX = m_coords.invOriginX + static_cast<float>(m_waystoneCol) * m_coords.invCellW + m_coords.invCellW * 0.5f;
                float invY = m_coords.invOriginY + static_cast<float>(m_waystoneRow) * m_coords.invCellH + m_coords.invCellH * 0.5f;
                auto invPt = m_coords.ScalePoint(invX, invY, screenW, screenH);

                kmbox.KeyDown(VK_CONTROL);
                kmbox.MoveMouseSmooth(static_cast<int>(invPt.x), static_cast<int>(invPt.y), m_curveGen, 10, 1);
                kmbox.ClickMouse(1, 40);
                kmbox.KeyUp(VK_CONTROL);
                CoreLog("[MapDeviceHandler] PlaceWaystone: Targeted slot [" +
                        std::to_string(m_waystoneCol) + "," + std::to_string(m_waystoneRow) + "]");
            }

            if (m_useTablet) {
                m_state = MapDeviceState::PlaceTablet;
                CoreLog("[MapDeviceHandler] State: PlaceWaystone -> PlaceTablet");
            } else {
                m_state = MapDeviceState::Activate;
                CoreLog("[MapDeviceHandler] State: PlaceWaystone -> Activate");
            }
            m_lastStepMs = nowMs;
            return true;
        }

        case MapDeviceState::PlaceTablet: {
            // Dat Tablet phu tro
            auto tabPt = m_coords.ScalePoint(m_coords.tabletSlotX, m_coords.tabletSlotY, screenW, screenH);
            kmbox.MoveMouseSmooth(static_cast<int>(tabPt.x), static_cast<int>(tabPt.y), m_curveGen, 12, 1);
            kmbox.ClickMouse(1, 40);
            m_state = MapDeviceState::Activate;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] State: PlaceTablet -> Activate");
            return true;
        }

        case MapDeviceState::Activate: {
            // TRAVERSE @1080p (806, 563) — POE2 không dùng nút Activate kiểu POE1 (420, 660)
            const float travX = (m_coords.traverseButtonX > 0.0f) ? m_coords.traverseButtonX : m_coords.activateButtonX;
            const float travY = (m_coords.traverseButtonY > 0.0f) ? m_coords.traverseButtonY : m_coords.activateButtonY;
            auto actPt = m_coords.ScalePoint(travX, travY, screenW, screenH);
            kmbox.MoveMouseSmooth(static_cast<int>(actPt.x), static_cast<int>(actPt.y), m_curveGen, 12, 1);
            kmbox.ClickMouse(1, 40);

            m_hasPortalWorldPos = false;
            m_targetPortalEntityId = 0;
            m_state = MapDeviceState::WaitForPortals;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] State: Activate TRAVERSE (" +
                    std::to_string(static_cast<int>(actPt.x)) + "," +
                    std::to_string(static_cast<int>(actPt.y)) + ") -> WaitForPortals");
            return true;
        }

        case MapDeviceState::WaitForPortals: {
            if (nowMs - m_lastStepMs < m_coords.activateDelayMs) {
                return false;
            }

            int bestPortal = -1;
            float bestDist = 99999.0f;
            for (uint32_t i = 0; i < packet.entityCount; ++i) {
                const auto& ent = packet.entities[i];
                if (ent.type != 3 || (ent.extraFlags & 4)) continue;
                if (m_targetDeviceEntityId != 0 && ent.id == m_targetDeviceEntityId) continue;
                std::string lower(ent.name);
                for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
                const bool namedPortal = lower.find("portal") != std::string::npos;
                const bool unnamedOk = lower.find("device") == std::string::npos &&
                                       lower.find("atlas") == std::string::npos;
                if (!namedPortal && !unnamedOk) continue;
                float dx = ent.posX - packet.player.posX;
                float dy = ent.posY - packet.player.posY;
                float d = std::sqrt(dx * dx + dy * dy);
                if (d < bestDist) {
                    bestDist = d;
                    bestPortal = static_cast<int>(i);
                }
            }

            if (bestPortal < 0) {
                // Retry Polling: Cho phep retry toi da 8000ms tinh tu luc Click TRAVERSE
                // tranh fail-closed qua som khi hoat anh mo cong cua POE2 mat tu 2.5 - 5s
                constexpr uint64_t maxPortalWaitMs = 8000;
                if (nowMs - m_lastStepMs < maxPortalWaitMs) {
                    return false; // Tiep tuc cho portal xuat hien o tick tiep theo
                }

                ReleaseApproachKeys(kmbox);
                m_state = MapDeviceState::Failed;
                CoreLog("[MapDeviceHandler] WaitForPortals fail-closed: khong co portal type=3 sau TRAVERSE sau " +
                        std::to_string(nowMs - m_lastStepMs) + "ms.");
                return false;
            }

            const auto& portal = packet.entities[bestPortal];
            m_targetPortalEntityId = portal.id;
            m_portalWorldX = portal.posX;
            m_portalWorldY = portal.posY;
            m_hasPortalWorldPos = true;
            m_panelsClosed = false;
            m_state = MapDeviceState::EnterPortal;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] State: WaitForPortals -> EnterPortal id=" +
                    std::to_string(m_targetPortalEntityId));
            return true;
        }

        case MapDeviceState::EnterPortal: {
            if (!m_panelsClosed) {
                kmbox.KeyPress(0x2C, 40); // Space — đóng Atlas/panel trước khi vào cổng
                m_panelsClosed = true;
                m_lastStepMs = nowMs;
                CoreLog("[MapDeviceHandler] EnterPortal: Space close panels");
                return true;
            }

            if (m_isZoomedIn) {
                kmbox.Wheel(-120 * static_cast<int>(m_zoomSteps));
                m_isZoomedIn = false;
                CoreLog("[INV-NAV-TERRAIN-COMMERCIAL] Phuc hoi Camera Zoom ve mac dinh (-480 wheel).");
            }

            float portalX = m_hasPortalWorldPos ? m_portalWorldX : 0.0f;
            float portalY = m_hasPortalWorldPos ? m_portalWorldY : 0.0f;
            if (m_targetPortalEntityId > 0) {
                for (uint32_t i = 0; i < packet.entityCount; ++i) {
                    const auto& ent = packet.entities[i];
                    if (ent.id == m_targetPortalEntityId) {
                        portalX = ent.posX;
                        portalY = ent.posY;
                        m_hasPortalWorldPos = true;
                        break;
                    }
                }
            }

            if (!m_hasPortalWorldPos) {
                ReleaseApproachKeys(kmbox);
                m_state = MapDeviceState::Failed;
                CoreLog("[MapDeviceHandler] EnterPortal fail-closed: khong co toa do portal (cam fallback 960,400).");
                return false;
            }

            float dx = portalX - packet.player.posX;
            float dy = portalY - packet.player.posY;
            float dist = std::sqrt(dx * dx + dy * dy);
            if (dist > m_coords.interactRadius) {
                const auto wasd = common::WorldToWasd(dx, dy);
                std::vector<uint8_t> keys;
                if (wasd.up) keys.push_back(0x1A);
                if (wasd.down) keys.push_back(0x16);
                if (wasd.left) keys.push_back(0x04);
                if (wasd.right) keys.push_back(0x07);
                HoldApproachWasd(keys, kmbox);
                m_lastStepMs = nowMs;
                return true;
            }

            ReleaseApproachKeys(kmbox);
            auto pt = common::WorldToScreenIsometric(dx, dy, dist * 14.0f, screenW, screenH, 80.0f);
            kmbox.MoveMouseSmooth(static_cast<int>(pt.x), static_cast<int>(pt.y), m_curveGen, 12, 1);
            // Trong POE2 KBM, bước vào Portal là Click chuột trái (LMB) vào cổng
            int dwellMs = m_curveGen.GenerateDwellTimeMs(45.0f, 8.0f);
            kmbox.ClickMouse(1, dwellMs);

            m_mapsOpenedCount++;
            m_state = MapDeviceState::Completed;
            m_lastStepMs = nowMs;
            CoreLog("[MapDeviceHandler] >> Chu trinh Map Device thanh cong! (Tong so maps da mo: " +
                    std::to_string(m_mapsOpenedCount) + ") <<");
            return true;
        }

        case MapDeviceState::Completed:
        case MapDeviceState::Failed:
        case MapDeviceState::Idle:
        default:
            return false;
    }
}

void MapDeviceHandler::ReleaseApproachKeys(KMBoxNet& kmbox) {
    for (uint8_t hid : m_heldApproachHids) {
        kmbox.KeyUp(hid);
    }
    m_heldApproachHids.clear();
}

void MapDeviceHandler::HoldApproachWasd(const std::vector<uint8_t>& hids, KMBoxNet& kmbox) {
    for (uint8_t held : m_heldApproachHids) {
        bool keep = false;
        for (uint8_t next : hids) {
            if (next == held) {
                keep = true;
                break;
            }
        }
        if (!keep) {
            kmbox.KeyUp(held);
        }
    }
    for (uint8_t next : hids) {
        bool already = false;
        for (uint8_t held : m_heldApproachHids) {
            if (held == next) {
                already = true;
                break;
            }
        }
        if (!already) {
            kmbox.KeyDown(next);
        }
    }
    m_heldApproachHids = hids;
}
