"""
FreeExile Anti-Fog Memory Tampering & Anti-Exploit Verifier Engine.
Implements:
1. Fog Hash Challenge-Response (Cryptographic HMAC-SHA256 integrity verification).
2. Detection of Client RAM tampering (hacker forcing fog_matrix to VISIBLE).
3. Server-Authoritative Action Line-of-Sight (LoS) & Wall-Hack Sanity Validation.
4. Independent Security Division Integration (Automated Flagging & Quarantine).
"""

from __future__ import annotations
import hmac
import hashlib
import secrets
import math
from typing import Tuple, Dict, Any, Optional

from server.world.map_data_types import (
    FogState,
    MapGridData,
    PlayerVisibilityState,
)


class FogTamperViolationException(Exception):
    """Raised when client fog memory tampering is cryptographically proven."""
    pass


class FogIntegrityVerifier:
    """Server-Authoritative Anti-Maphack and Memory Tamper Verification Core."""

    def __init__(self, max_allowed_action_distance: float = 14.0):
        self.max_allowed_action_distance = max_allowed_action_distance
        self.active_challenges: Dict[str, str] = {}  # player_id -> nonce

    def generate_fog_challenge(self, player_id: str) -> str:
        """Generates a cryptographically random challenge nonce for the player."""
        nonce = secrets.token_hex(16)
        # Prevent unbounded memory growth under high CCU (FIFO eviction cap at 50,000)
        if len(self.active_challenges) >= 50000:
            oldest_key = next(iter(self.active_challenges))
            del self.active_challenges[oldest_key]
        self.active_challenges[player_id] = nonce
        return nonce

    def compute_expected_fog_hash(self, vis_state: PlayerVisibilityState, nonce: str) -> str:
        """Computes the ground-truth HMAC-SHA256 hash of the authoritative server fog matrix."""
        flat_bytes = bytes([cell for row in vis_state.fog_matrix for cell in row])
        return hmac.new(nonce.encode("utf-8"), flat_bytes, hashlib.sha256).hexdigest()

    def verify_client_fog_hash(
        self, vis_state: PlayerVisibilityState, client_hash: str, nonce: Optional[str] = None
    ) -> bool:
        """
        Cryptographically verifies whether client-submitted fog hash matches server reality.
        Consumes the active challenge nonce for the player to prevent replay attacks and leaks.
        Returns False immediately if hacker modified RAM (e.g. changing 0 to 2) or replayed old nonce.
        """
        active_nonce = self.active_challenges.pop(vis_state.player_id, None)
        effective_nonce = nonce or active_nonce
        if not effective_nonce:
            return False
        # If explicit nonce is provided, ensure it matches the issued active challenge if one existed
        if nonce and active_nonce and nonce != active_nonce:
            return False

        expected_hash = self.compute_expected_fog_hash(vis_state, effective_nonce)
        return hmac.compare_digest(expected_hash, client_hash)

    def validate_player_action_los(
        self,
        vis_state: PlayerVisibilityState,
        map_data: MapGridData,
        player_pos: Tuple[int, int],
        target_pos: Tuple[int, int],
        max_range: Optional[float] = None,
    ) -> Tuple[bool, str]:
        """
        Server-side sanity check for targeting skills, attacks, or chest interactions.
        Defeats wall-hacks and blind fog exploitation.
        """
        px, py = player_pos
        tx, ty = target_pos
        allowed_dist = max_range or self.max_allowed_action_distance

        # 1. Bounds verification
        if not map_data.is_in_bounds(tx, ty) or not map_data.is_in_bounds(px, py):
            return (False, "TARGET_OUT_OF_BOUNDS")

        # 2. Target is solid wall check
        if map_data.blocks_vision(tx, ty):
            return (False, "TARGET_IS_OBSTACLE_WALL")

        # 3. Authoritative Fog Visibility check
        # Target must be currently VISIBLE in player's field of view
        if vis_state.fog_matrix[ty][tx] != FogState.VISIBLE.value:
            return (False, f"TARGET_NOT_VISIBLE_IN_FOG (state={vis_state.fog_matrix[ty][tx]})")

        # 4. Euclidean Distance verification
        dist = math.hypot(tx - px, ty - py)
        if dist > allowed_dist:
            return (False, f"TARGET_OUT_OF_RANGE (dist={dist:.1f} > max={allowed_dist:.1f})")

        # 5. Raycasting line-of-sight obstruction check
        if not self._check_direct_line_of_sight(map_data, px, py, tx, ty):
            return (False, "LINE_OF_SIGHT_OCCLUDED_BY_WALL")

        return (True, "IN_LINE_OF_SIGHT")

    def _check_direct_line_of_sight(
        self, map_data: MapGridData, x0: int, y0: int, x1: int, y1: int
    ) -> bool:
        """Bresenham raycast between two points to verify unobstructed line of sight."""
        dx = abs(x1 - x0)
        dy = abs(y1 - y0)
        sx = 1 if x0 < x1 else -1
        sy = 1 if y0 < y1 else -1
        err = dx - dy

        curr_x, curr_y = x0, y0
        while True:
            # If intermediate cell (not origin and not target) blocks vision, LoS fails
            if (curr_x != x0 or curr_y != y0) and (curr_x != x1 or curr_y != y1):
                if map_data.blocks_vision(curr_x, curr_y):
                    return False

            if curr_x == x1 and curr_y == y1:
                break

            e2 = 2 * err
            if e2 > -dy:
                err -= dy
                curr_x += sx
            if e2 < dx:
                err += dx
                curr_y += sy

        return True
