import asyncio
import math
import random
import time
from typing import Tuple, List

# Type hint for Playwright Page
try:
    from playwright.async_api import Page
except ImportError:
    # Allow running without playwright installed for unit testing logic
    Page = None

class GhostCursorEngine:
    def __init__(self):
        # Empirical constants for Fitts's Law (T = a + b * log2(D/W + 1))
        # Initial values from ADD, can be tuned
        self.fitts_a = 0.1
        self.fitts_b = 0.15

    async def move_to(self, page: Page, target_x: int, target_y: int):
        """
        Generate human-like trajectory using composite Bezier curves
        with velocity-based submovement decomposition.
        """
        if page is None:
            raise RuntimeError("Playwright Page object is required for move_to")
            
        # Get start position (mock or real)
        # Playwright doesn't expose cursor position directly easily without tracking, 
        # but we can assume we track it or just start from a known location or 0,0 
        # if it's the first move. 
        # For simplicity/robustness, we might need to assume 0,0 or last known.
        # However, to be realistic, we usually chain moves. 
        # Let's try to get it from mouse object if possible, or just assume (0,0) if unknown.
        # Actually, page.mouse has no 'position' property. 
        # We will track current_position internally if needed, but for now let's assume 
        # we can't easily get it and start from (0,0) or passed in 'start' if we refactor.
        # Better approach: We should track where WE moved the mouse last.
        
        # NOTE: Since we can't query current mouse position easily in standard Playwright
        # without injecting tracking scripts, we will assume (0,0) for the very first move 
        # and then track it? No, that's risky.
        # A common trick is to mouse.move(x,y) to a known safe spot or just start.
        # IMPORTANT: The ADD pseudo-code called 'await self._get_cursor_position(page)'.
        # We will implement that helper.
        current_x, current_y = await self._get_cursor_position(page)
        
        # Calculate distance for submovement count
        distance = math.sqrt((target_x - current_x)**2 + 
                           (target_y - current_y)**2)
        
        # Human submovements: 1-3 for short distances, up to 5 for long
        # Cap max distance to avoid excessive submovements
        num_submovements = min(5, max(1, int(distance / 300)))
        
        waypoints = self._generate_waypoints(
            (current_x, current_y),
            (target_x, target_y),
            num_submovements
        )
        
        for i in range(len(waypoints) - 1):
            await self._execute_submovement(page, waypoints[i], waypoints[i+1])

    async def _get_cursor_position(self, page: Page) -> Tuple[float, float]:
        """
        Get current cursor position via JS injection.
        Requires a mouse-tracking script to be active, or just check a global var.
        If not present, default to (0,0).
        """
        # Inject a small tracker if not present? 
        # Or just rely on previous known. 
        # Let's try to read a global variable we might have injected, or standard generic check.
        # Simplest fallback: (0,0)
        try:
            # Requires a prior script to track 'window.mouseX/Y'.
            # If not present, we can't know.
            # We'll return 0,0 for robustness if undefined.
            pos = await page.evaluate("""() => {
                if (window.mousePos) return window.mousePos;
                return {x: 0, y: 0};
            }""")
            return (pos['x'], pos['y'])
        except Exception:
            return (0, 0)
    
    def _generate_waypoints(self, start: Tuple[float, float], end: Tuple[float, float], count: int) -> List[Tuple[float, float]]:
        """
        Generate intermediate waypoints with Gaussian perturbation
        to simulate motor control noise.
        """
        waypoints = [start]
        
        for i in range(1, count):
            t = i / count
            # Linear interpolation
            lx = start[0] + t * (end[0] - start[0])
            ly = start[1] + t * (end[1] - start[1])
            
            # Add perpendicular noise (overshooting)
            # Vector from start to end
            dx = end[0] - start[0]
            dy = end[1] - start[1]
            angle = math.atan2(dy, dx)
            perp_angle = angle + math.pi / 2
            
            # Noise magnitude
            noise_magnitude = random.gauss(0, 10 * (1 - t)) # Reduce noise near target
            
            # Perturb
            x = lx + noise_magnitude * math.cos(perp_angle)
            y = ly + noise_magnitude * math.sin(perp_angle)
            
            waypoints.append((x, y))
        
        waypoints.append(end)
        return waypoints
    
    async def _execute_submovement(self, page: Page, start: Tuple[float, float], end: Tuple[float, float]):
        """
        Execute single submovement with velocity profile matching Fitts's Law.
        """
        distance = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2)
        if distance < 1:
            return

        # Generate Bezier control points
        control1, control2 = self._generate_bezier_controls(start, end)
        
        # Calculate movement time from Fitts's Law
        # W (target width) is unknown, assume generic small interaction target size ~20px? 
        # Or just use distance scaling. 
        # ADD suggests: T = a + b * log2(D/W + 1)
        # Let's tune W relative to D or constant.
        w_approx = 50 
        movement_time = self.fitts_a + self.fitts_b * math.log2(distance / w_approx + 1)
        
        # Clamp Movement Time to be realistic (human reaction time etc)
        movement_time = max(0.1, movement_time) 

        # Sample Bezier curve
        # Steps depends on duration and roughly 60fps or similar
        steps = max(10, int(distance / 5)) 
        
        delay_per_step = movement_time / steps
        
        for i in range(1, steps + 1):
            t = i / steps
            px, py = self._bezier_point(t, start, control1, control2, end)
            
            # In Playwright, mouse.move takes steps, but we are manually stepping 
            # to control velocity curve precisely.
            if page:
                await page.mouse.move(px, py)
            
            # Add slight jitter to timing?
            await asyncio.sleep(delay_per_step)
            
            # Update tracked position for next calls
            if page:
                await page.evaluate(f"window.mousePos = {{x: {px}, y: {py}}}")

    def _generate_bezier_controls(self, start: Tuple[float, float], end: Tuple[float, float]) -> Tuple[Tuple[float, float], Tuple[float, float]]:
        """
        Generate two control points for cubic Bezier.
        """
        dx = end[0] - start[0]
        dy = end[1] - start[1]
        dist = math.sqrt(dx*dx + dy*dy)
        
        # Heuristic: Control points at 1/3 and 2/3 of distance, with some spread
        # Spread factor
        spread = 0.3 + random.uniform(-0.1, 0.1)
        
        # Perpendicular vector for curvature
        perp_x = -dy
        perp_y = dx
        
        # Random curvature intensity
        curve_strength = random.uniform(-0.5, 0.5) * dist * 0.2
        
        # P1
        p1_x = start[0] + dx * 0.3 + perp_x * curve_strength / dist
        p1_y = start[1] + dy * 0.3 + perp_y * curve_strength / dist
        
        # P2
        p2_x = start[0] + dx * 0.7 + perp_x * curve_strength / dist
        p2_y = start[1] + dy * 0.7 + perp_y * curve_strength / dist
        
        return (p1_x, p1_y), (p2_x, p2_y)

    def _bezier_point(self, t: float, p0: Tuple[float, float], p1: Tuple[float, float], p2: Tuple[float, float], p3: Tuple[float, float]) -> Tuple[float, float]:
        """Cubic Bezier curve evaluation."""
        x = (1-t)**3 * p0[0] + 3*(1-t)**2*t * p1[0] + \
            3*(1-t)*t**2 * p2[0] + t**3 * p3[0]
        y = (1-t)**3 * p0[1] + 3*(1-t)**2*t * p1[1] + \
            3*(1-t)*t**2 * p2[1] + t**3 * p3[1]
        return (x, y)
    
    async def random_micro_movement(self, page: Page):
        """
        Simulate fidgeting/drift.
        """
        if page is None:
            return
            
        current_x, current_y = await self._get_cursor_position(page)
        
        drift_x = random.gauss(0, 5)
        drift_y = random.gauss(0, 5)
        
        target_x = current_x + drift_x
        target_y = current_y + drift_y
        
        # Move slowly
        await page.mouse.move(target_x, target_y, steps=5)
        await page.evaluate(f"window.mousePos = {{x: {target_x}, y: {target_y}}}")
