"""AutoPOE2 - Minimap Navigation Adapter (Doc 69 §3.4 / Stage 5 In-Map).
================================================================================
Bộ thích ứng điều hướng Minimap kết nối MinimapFogExplorer với Character Controller:
- Tiếp nhận ảnh toàn màn hình hoặc crop Minimap.
- Sử dụng MinimapFogExplorer định vị Frontier mục tiêu tối ưu.
- Chuyển đổi vector hướng di chuyển thành góc độ và tổ hợp phím 8 hướng WASD
  (W, WD, D, SD, S, SA, A, WA) hoặc phím dừng NONE.
- Hỗ trợ cả 2 chế độ chiếu: Direct Screen Projection và Isometric World Projection.
- Tự động cắt vùng Minimap từ ảnh toàn màn hình thông qua Dynamic Resolution Scaler
  (Windowed 1189x891 PID 22632, 1440p, 1080p, 720p).
- Đảm bảo thời gian giữ phím (Dwell time) >= 300ms tuân thủ INV-WASD-MIN-DWELL.

Tuân thủ nghiêm ngặt:
- Rule 1: Python 3.11 Cold Path Tier 2.
- Rule 4: Module hóa độc lập trần file < 500 dòng.
- Rule 16: Google Docstrings chuẩn mực.
- Bất biến: INV-WASD-MIN-DWELL, INV-WASD-NO-BLIND-LMB, INV-NAV-FRONTIER-MOMENTUM.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
import math
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

import numpy as np

try:
    from PIL import Image
except ImportError:
    Image = None  # type: ignore[assignment]

from src.assistant_tool.navigation.minimap_camera_calibrator import (
    CameraMinimapProfile,
    MinimapCameraCalibrator,
)
from src.assistant_tool.navigation.minimap_fog_explorer import (
    ExplorationTarget,
    MinimapFogExplorer,
)
from src.common.coordinate_transform import (
    CoordinateScaler,
    Resolution,
    RES_1080P,
    RES_1440P,
    RES_WINDOWED_DEFAULT,
    resolve_resolution,
)


class WasdDirection(str, Enum):
    """8 hướng di chuyển WASD chuẩn và trạng thái đứng yên."""
    NORTH = "W"               # Lên (North)
    NORTH_EAST = "WD"         # Lên - Phải (North-East)
    EAST = "D"                # Phải (East)
    SOUTH_EAST = "SD"         # Xuống - Phải (South-East)
    SOUTH = "S"               # Xuống (South)
    SOUTH_WEST = "SA"         # Xuống - Trái (South-West)
    WEST = "A"                # Trái (West)
    NORTH_WEST = "WA"         # Lên - Trái (North-West)
    NONE = ""                 # Đứng yên / Không phát lệnh di chuyển


class ProjectionMode(str, Enum):
    """Chế độ chiếu góc di chuyển sang WASD."""
    DIRECT_SCREEN = "DIRECT_SCREEN"      # Ánh xạ trực tiếp góc 2D trên màn hình Minimap
    ISOMETRIC_WORLD = "ISOMETRIC_WORLD"  # Ánh xạ theo hệ tọa độ thế giới góc nhìn Isometric 45 độ


@dataclass
class WasdCommand:
    """Lệnh điều khiển di chuyển bàn phím WASD được sinh ra từ Minimap."""
    direction: WasdDirection
    keys: Tuple[str, ...]
    vector: Tuple[float, float]          # Vector đơn vị (vx, vy)
    angle_deg: float                     # Góc phương vị [0.0, 360.0)
    dwell_sec: float                     # Thời gian giữ phím (>= 0.30s)
    screen_target: Tuple[int, int]       # Tọa độ pixel mục tiêu trên màn hình
    minimap_target: Tuple[int, int]      # Tọa độ pixel mục tiêu trên Minimap
    coverage_ratio: float                # Tỷ lệ bản đồ đã mở (0.0 .. 1.0)
    is_fully_explored: bool              # True nếu dọn map hoàn tất (>= 95% hoặc hết frontier)
    utility_score: float = 0.0           # Điểm tiện ích của mục tiêu


class MinimapNavigationAdapter:
    """Bộ chuyển đổi và điều phối dẫn đường sương mù cho Character Controller."""

    def __init__(
        self,
        target_resolution: Union[Resolution, Tuple[int, int]] = RES_1080P,
        grid_width: int = 64,
        grid_height: int = 64,
        projection_mode: ProjectionMode = ProjectionMode.DIRECT_SCREEN,
        default_dwell_sec: float = 0.35,
        target_coverage: float = 0.95,
        explorer: Optional[MinimapFogExplorer] = None,
        calibrator: Optional[MinimapCameraCalibrator] = None,
    ) -> None:
        """Khởi tạo Minimap Navigation Adapter.

        Args:
            target_resolution: Độ phân giải game client đang chạy.
            grid_width: Chiều ngang lưới thám hiểm.
            grid_height: Chiều dọc lưới thám hiểm.
            projection_mode: Chế độ chiếu góc (DIRECT_SCREEN hoặc ISOMETRIC_WORLD).
            default_dwell_sec: Thời gian giữ phím mặc định (tối thiểu 0.30s).
            target_coverage: Ngưỡng bao phủ hoàn thành map (mặc định 95%).
            explorer: Thể hiện MinimapFogExplorer tùy chọn (nếu có sẵn).
            calibrator: Bộ hiệu chuẩn góc xoay và zoom camera/minimap tùy chọn.
        """
        self.resolution = resolve_resolution(target_resolution)
        self.projection_mode = projection_mode
        self.default_dwell_sec = max(0.30, float(default_dwell_sec))
        self.target_coverage = target_coverage
        self.calibrator = calibrator

        self.explorer = explorer or MinimapFogExplorer(
            grid_width=grid_width,
            grid_height=grid_height,
            target_resolution=self.resolution,
            target_coverage=self.target_coverage,
        )

    def set_resolution(self, res_input: Union[Resolution, Tuple[int, int]]) -> None:
        """Cập nhật độ phân giải màn hình runtime."""
        self.resolution = resolve_resolution(res_input)
        self.explorer.set_target_resolution(self.resolution)
        if self.calibrator is not None:
            self.calibrator.scaler = CoordinateScaler(
                source_res=RES_1080P,
                target_res=self.resolution,
            )
            self.calibrator.resolution = self.resolution

    def set_camera_calibration(
        self,
        zoom_level: Optional[float] = None,
        orientation_deg: Optional[float] = None,
    ) -> None:
        """Cập nhật hoặc khởi tạo bộ hiệu chuẩn camera/minimap."""
        if self.calibrator is None:
            self.calibrator = MinimapCameraCalibrator(
                base_zoom=zoom_level if zoom_level is not None else 1.0,
                base_orientation_deg=orientation_deg if orientation_deg is not None else 0.0,
                target_resolution=self.resolution,
            )
        else:
            self.calibrator.set_calibration(
                zoom_level=zoom_level,
                orientation_deg=orientation_deg,
            )

    def apply_map_layout_intelligence(self, map_name: str) -> Dict[str, Any]:
        """Đồng bộ đặc tính địa hình mở của các map T15 cho Monk CI (Doc 56 / CMD-DIV3-STAGE3-STAGE5-REG1-20260918).
        
        Tối ưu hóa góc quét Frontier Fog và dwell time theo từng dạng layout:
        - Map mở rộng (Deserted, Dunes, Plains, Beach): Dwell dài hơn (0.45s), tăng ngưỡng bao phủ 92%.
        - Map pháo đài/mỏ đá (Fortress, Mineshaft, Bastion): Dwell vừa phải (0.35s), tăng mật độ quét để phát hiện ngách.
        - Map đầm lầy/dòng suối (Creek, Marsh, Riverbank): Dwell 0.38s, xử lý địa hình quanh co.
        """
        name_lower = map_name.lower().strip()
        settings = {
            "map_name": map_name,
            "layout_type": "STANDARD",
            "dwell_sec": self.default_dwell_sec,
            "target_coverage": self.target_coverage,
        }
        if any(k in name_lower for k in ["deserted", "dunes", "plains", "beach"]):
            settings["layout_type"] = "WIDE_OPEN"
            self.default_dwell_sec = 0.45
            self.target_coverage = 0.92
        elif any(k in name_lower for k in ["fortress", "mineshaft", "bastion"]):
            settings["layout_type"] = "SEMI_OPEN_CORRIDOR"
            self.default_dwell_sec = 0.35
            self.target_coverage = 0.90
        elif any(k in name_lower for k in ["creek", "marsh", "riverbank"]):
            settings["layout_type"] = "WETLAND_MEANDER"
            self.default_dwell_sec = 0.38
            self.target_coverage = 0.90

        settings["dwell_sec"] = self.default_dwell_sec
        settings["target_coverage"] = self.target_coverage
        return settings

    def auto_calibrate_from_hud(self, ocr_text: str) -> bool:
        """Tự động phát hiện zoom từ text banner HUD."""
        if self.calibrator is None:
            self.calibrator = MinimapCameraCalibrator(target_resolution=self.resolution)
        detected = self.calibrator.detect_zoom_from_banner_text(ocr_text)
        if detected is not None:
            self.calibrator.set_calibration(zoom_level=detected)
            return True
        return False

    def extract_minimap_crop(
        self,
        fullscreen_img: Union[np.ndarray, Image.Image, str, Path],
    ) -> Image.Image:
        """Trích xuất vùng ảnh Minimap góc trên bên phải từ ảnh chụp toàn màn hình."""
        bx1, by1, bx2, by2 = self.explorer.minimap_screen_bbox

        if isinstance(fullscreen_img, np.ndarray):
            h, w = fullscreen_img.shape[:2]
            cx1 = max(0, min(w - 1, bx1))
            cy1 = max(0, min(h - 1, by1))
            cx2 = max(cx1 + 1, min(w, bx2))
            cy2 = max(cy1 + 1, min(h, by2))
            crop_arr = fullscreen_img[cy1:cy2, cx1:cx2]
            if Image is not None:
                return Image.fromarray(crop_arr)
            return crop_arr  # type: ignore[return-value]
        elif isinstance(fullscreen_img, (str, Path)):
            pil_img = Image.open(fullscreen_img)
        else:
            pil_img = fullscreen_img

        w, h = pil_img.size

        # Giới hạn an toàn trong kích thước ảnh
        cx1 = max(0, min(w - 1, bx1))
        cy1 = max(0, min(h - 1, by1))
        cx2 = max(cx1 + 1, min(w, bx2))
        cy2 = max(cy1 + 1, min(h, by2))

        return pil_img.crop((cx1, cy1, cx2, cy2))

    @staticmethod
    def vector_to_8way_direction(
        dx: float,
        dy: float,
        mode: ProjectionMode = ProjectionMode.DIRECT_SCREEN,
        deadzone: float = 0.05,
    ) -> Tuple[WasdDirection, Tuple[str, ...], float]:
        """Chuyển đổi vector (dx, dy) thành 8 hướng WASD và góc độ.

        Args:
            dx: Độ lệch theo trục X.
            dy: Độ lệch theo trục Y.
            mode: Chế độ chiếu DIRECT_SCREEN hoặc ISOMETRIC_WORLD.
            deadzone: Ngưỡng vùng chết không phát lệnh.

        Returns:
            Tuple[WasdDirection, Tuple[str, ...], float]: (hướng, phím, góc_deg).
        """
        norm = math.hypot(dx, dy)
        if norm < deadzone:
            return WasdDirection.NONE, (), 0.0

        ndx = dx / norm
        ndy = dy / norm

        if mode == ProjectionMode.ISOMETRIC_WORLD:
            # Hệ tọa độ Isometric PoE2 (tham chiếu math2d.hpp / win32_input.py)
            inv_sqrt2 = 0.70710678
            sx = (ndx - ndy) * inv_sqrt2
            sy = (ndx + ndy) * inv_sqrt2
            threshold = 0.38

            keys_list: List[str] = []
            if sy > threshold:
                keys_list.append("W")
            elif sy < -threshold:
                keys_list.append("S")

            if sx > threshold:
                keys_list.append("D")
            elif sx < -threshold:
                keys_list.append("A")

            # Ánh xạ sang enum WasdDirection
            combo = "".join(sorted(keys_list, key=lambda k: ["W", "S", "A", "D"].index(k)))
            mapping = {
                "W": WasdDirection.NORTH,
                "WD": WasdDirection.NORTH_EAST,
                "D": WasdDirection.EAST,
                "SD": WasdDirection.SOUTH_EAST,
                "S": WasdDirection.SOUTH,
                "SA": WasdDirection.SOUTH_WEST,
                "A": WasdDirection.WEST,
                "WA": WasdDirection.NORTH_WEST,
            }
            direction = mapping.get(combo, WasdDirection.NONE)
            angle_deg = (math.degrees(math.atan2(sy, sx))) % 360.0
            return direction, tuple(keys_list), angle_deg

        # Mặc định DIRECT_SCREEN:
        # Trong hệ tọa độ màn hình Minimap: X sang phải (East), Y xuống dưới (South)
        # Góc tính từ trục X dương theo chiều kim đồng hồ: [0, 360)
        angle_deg = (math.degrees(math.atan2(ndy, ndx))) % 360.0

        # Phân 8 cung góc 45 độ (mỗi cung +- 22.5 độ)
        # 0 deg: East (D)
        # 45 deg: South-East (SD)
        # 90 deg: South (S)
        # 135 deg: South-West (SA)
        # 180 deg: West (A)
        # 225 deg: North-West (WA)
        # 270 deg: North (W)
        # 315 deg: North-East (WD)
        if 337.5 <= angle_deg or angle_deg < 22.5:
            return WasdDirection.EAST, ("D",), angle_deg
        if 22.5 <= angle_deg < 67.5:
            return WasdDirection.SOUTH_EAST, ("S", "D"), angle_deg
        if 67.5 <= angle_deg < 112.5:
            return WasdDirection.SOUTH, ("S",), angle_deg
        if 112.5 <= angle_deg < 157.5:
            return WasdDirection.SOUTH_WEST, ("S", "A"), angle_deg
        if 157.5 <= angle_deg < 202.5:
            return WasdDirection.WEST, ("A",), angle_deg
        if 202.5 <= angle_deg < 247.5:
            return WasdDirection.NORTH_WEST, ("W", "A"), angle_deg
        if 247.5 <= angle_deg < 292.5:
            return WasdDirection.NORTH, ("W",), angle_deg
        return WasdDirection.NORTH_EAST, ("W", "D"), angle_deg

    def compute_navigation_step(
        self,
        minimap_crop_img: Union[np.ndarray, Image.Image],
        player_grid_pos: Optional[Tuple[float, float]] = None,
        player_velocity: Tuple[float, float] = (0.0, 0.0),
    ) -> WasdCommand:
        """Phân tích ảnh crop Minimap và sinh lệnh di chuyển WASD tối ưu."""
        target: ExplorationTarget = self.explorer.analyze_minimap_and_route(
            minimap_img=minimap_crop_img,
            player_grid_pos=player_grid_pos,
            player_velocity=player_velocity,
        )

        if target.is_fully_explored or (target.direction_vector == (0.0, 0.0)):
            return WasdCommand(
                direction=WasdDirection.NONE,
                keys=(),
                vector=(0.0, 0.0),
                angle_deg=0.0,
                dwell_sec=0.0,
                screen_target=target.target_screen_xy,
                minimap_target=target.target_minimap_xy,
                coverage_ratio=target.coverage_ratio,
                is_fully_explored=target.is_fully_explored,
                utility_score=target.utility_score,
            )

        dx, dy = target.direction_vector
        dwell_sec = self.default_dwell_sec

        # Bù trừ góc xoay và điều chỉnh dwell time theo độ zoom nếu có calibrator
        if self.calibrator is not None:
            dx, dy = self.calibrator.transform_vector(dx, dy)
            dwell_sec = self.calibrator.adjust_dwell_time(self.default_dwell_sec)

        direction, keys, angle_deg = self.vector_to_8way_direction(
            dx=dx,
            dy=dy,
            mode=self.projection_mode,
        )

        return WasdCommand(
            direction=direction,
            keys=keys,
            vector=(dx, dy),
            angle_deg=angle_deg,
            dwell_sec=dwell_sec,
            screen_target=target.target_screen_xy,
            minimap_target=target.target_minimap_xy,
            coverage_ratio=target.coverage_ratio,
            is_fully_explored=target.is_fully_explored,
            utility_score=target.utility_score,
        )

    def process_fullscreen_frame(
        self,
        fullscreen_img: Union[np.ndarray, Image.Image],
        player_velocity: Tuple[float, float] = (0.0, 0.0),
    ) -> WasdCommand:
        """Quy trình toàn diện: Chụp toàn màn hình -> Cắt Minimap -> Xuất lệnh WASD."""
        minimap_crop = self.extract_minimap_crop(fullscreen_img)
        return self.compute_navigation_step(
            minimap_crop_img=minimap_crop,
            player_velocity=player_velocity,
        )

    def dispatch_to_controller(
        self,
        command: WasdCommand,
        sender_func: Optional[Callable[[float, float, float], None]] = None,
    ) -> bool:
        """Gửi lệnh di chuyển tới Character Controller qua hàm phát phím WASD."""
        if command.direction == WasdDirection.NONE:
            return False

        if sender_func is not None:
            vx, vy = command.vector
            sender_func(vx, vy, command.dwell_sec)
            return True

        return False
