#!/usr/bin/env python3
"""
Click Element Primitive Operation

This module provides element clicking functionality with human-like mouse movement.
Uses Bezier curves and erratic movements to simulate natural human behavior.
"""

import asyncio
import random
import math
from typing import Dict, Any, Tuple, List
from patchright.async_api  import Page

from lib.page_management import ManagedPage


class ClickElement:
    """
    Click Element with Human-like Mouse Movement

    Implements realistic mouse movement using Bezier curves with random variations
    to simulate natural human behavior. The movement includes:
    - Random starting position
    - Curved path using cubic Bezier curves
    - Random variations/jitter along the path
    - Variable speed (slower at start/end, faster in middle)

    Usage:
        clicker = ClickElement()
        result = await clicker.click_selector(managed_page, selector, slowmo=1000)
    """

    def __init__(self):
        """Initialize the click element handler."""
        print("🖱️ ClickElement initialized")

    async def click_selector(self, managed_page: ManagedPage, selector: str, slowmo: int = 100) -> Dict[str, Any]:
        """
        Click an element on the page with human-like mouse movement.

        Args:
            page (Page): Playwright page to use
            selector (str): CSS selector of element to click
            slowmo (int): Base delay in milliseconds for movement timing (default: 100)

        Returns:
            Dict containing click result and element information
        """
        try:
            print(f"👆 Clicking element '{selector}'")

            # Check if element exists and is visible
            element = await managed_page.page.query_selector(selector)
            if not element:
                raise Exception(f"Element '{selector}' not found")

            is_visible = await element.is_visible()
            if not is_visible:
                raise Exception(f"Element '{selector}' is not visible")

            # Get element bounding box
            box = await element.bounding_box()
            if not box:
                raise Exception(f"Could not get bounding box for element '{selector}'")

            # Calculate target position (slightly randomized within element)
            target_x = box["x"] + box["width"] / 2 + random.uniform(-box["width"] * 0.2, box["width"] * 0.2)
            target_y = box["y"] + box["height"] / 2 + random.uniform(-box["height"] * 0.2, box["height"] * 0.2)

            # Move mouse with human-like curved path
            await self._move_mouse_human_like(managed_page, target_x, target_y, slowmo)

            # Click element
            await managed_page.page.mouse.click(target_x, target_y)

            # update last known mouse position
            managed_page.last_known_mouse_x = target_x
            managed_page.last_known_mouse_y = target_y

            # Wait for potential navigation/changes
            await asyncio.sleep(slowmo / 1000)

            result = {
                "success": True,
                "selector": selector,
                "url": managed_page.page.url
            }

            print(f"✅ Element clicked: {selector}")
            return result

        except Exception as e:
            error_result = {
                "success": False,
                "selector": selector,
                "error": str(e)
            }
            print(f"❌ Element click failed: {e}")
            return error_result

    async def click_position(self, managed_page: ManagedPage, x: float, y: float, slowmo: int = 100) -> Dict[str, Any]:
        """
        Click at specific (x, y) position on the page with human-like mouse movement.

        Args:
            page (Page): Playwright page to use
            x (float): X coordinate to click
            y (float): Y coordinate to click
            slowmo (int): Base delay in milliseconds for movement timing (default: 100)

        Returns:
            Dict containing click result and position information
        """
        try:
            print(f"👆 Clicking position ({x}, {y})")

            # Move mouse with human-like curved path
            await self._move_mouse_human_like(managed_page, x, y, slowmo)

            # Click at position
            await managed_page.page.mouse.click(x, y)

            # update last known mouse position
            managed_page.last_known_mouse_x = x
            managed_page.last_known_mouse_y = y

            # Wait for potential navigation/changes
            await asyncio.sleep(slowmo / 1000)

            result = {
                "success": True,
                "position": {"x": x, "y": y},
                "url": managed_page.page.url
            }

            print(f"✅ Position clicked: ({x}, {y})")
            return result

        except Exception as e:
            error_result = {
                "success": False,
                "position": {"x": x, "y": y},
                "error": str(e)
            }
            print(f"❌ Position click failed: {e}")
            return error_result


    async def _move_mouse_human_like(self, managed_page: ManagedPage, target_x: float, target_y: float, slowmo: int):
        """
        Move mouse from random starting position to target with human-like curved path.

        Uses cubic Bezier curves with random control points to create natural-looking
        curved movements. Adds jitter and variable speed for realism.

        Args:
            page (Page): Playwright page
            target_x (float): Target X coordinate
            target_y (float): Target Y coordinate
            slowmo (int): Base delay for timing
        """

        # Random starting position (away from target)
        start_x = managed_page.last_known_mouse_x  
        start_y = managed_page.last_known_mouse_y  

        # Initial random mouse movements (micro adjustments)
        await managed_page.page.mouse.move(start_x, start_y, steps=1)
        await asyncio.sleep(random.uniform(0.01, 0.05))

        # Generate curved path using Bezier curve
        path_points = self._generate_bezier_path(start_x, start_y, target_x, target_y)

        # Move along the path with variable speed and jitter
        total_points = len(path_points)
        for i, (x, y) in enumerate(path_points):
            # Add jitter (small random deviations)
            jitter_x = random.uniform(-1.5, 1.5)
            jitter_y = random.uniform(-1.5, 1.5)

            final_x = x + jitter_x
            final_y = y + jitter_y

            # Move to point
            await managed_page.page.mouse.move(final_x, final_y, steps=1)

            # Variable speed: slower at start/end, faster in middle
            progress = i / total_points
            if progress < 0.2 or progress > 0.8:
                # Slower at start and end
                delay = slowmo / 1000 / total_points * random.uniform(1.5, 2.5)
            else:
                # Faster in middle
                delay = slowmo / 1000 / total_points * random.uniform(0.5, 1.0)

            await asyncio.sleep(delay)

    def _generate_bezier_path(self, start_x: float, start_y: float,
                              target_x: float, target_y: float,
                              num_points: int = 30) -> List[Tuple[float, float]]:
        """
        Generate a curved path using cubic Bezier curve with random control points.

        Args:
            start_x (float): Starting X coordinate
            start_y (float): Starting Y coordinate
            target_x (float): Target X coordinate
            target_y (float): Target Y coordinate
            num_points (int): Number of points along the curve (default: 30)

        Returns:
            List of (x, y) tuples representing the path
        """
        # Calculate distance for control point offset
        distance = math.sqrt((target_x - start_x)**2 + (target_y - start_y)**2)

        # Generate two control points for cubic Bezier curve
        # Control points are offset perpendicular to the direct line
        # This creates a natural curve

        # Midpoint
        mid_x = (start_x + target_x) / 2
        mid_y = (start_y + target_y) / 2

        # Perpendicular offset (creates the curve)
        curve_intensity = random.uniform(0.15, 0.35)  # How much curve
        offset = distance * curve_intensity

        # Angle perpendicular to the direct line
        dx = target_x - start_x
        dy = target_y - start_y
        angle = math.atan2(dy, dx) + math.pi / 2  # +90 degrees

        # Control point 1 (1/3 along the path)
        cp1_x = start_x + dx * 0.33 + math.cos(angle) * offset * random.uniform(0.5, 1.0)
        cp1_y = start_y + dy * 0.33 + math.sin(angle) * offset * random.uniform(0.5, 1.0)

        # Control point 2 (2/3 along the path)
        cp2_x = start_x + dx * 0.67 + math.cos(angle) * offset * random.uniform(-1.0, -0.5)
        cp2_y = start_y + dy * 0.67 + math.sin(angle) * offset * random.uniform(-1.0, -0.5)

        # Generate points along the Bezier curve
        points = []
        for i in range(num_points):
            t = i / (num_points - 1)  # Parameter from 0 to 1

            # Cubic Bezier curve formula
            # B(t) = (1-t)³P0 + 3(1-t)²t*P1 + 3(1-t)t²P2 + t³P3
            x = (
                (1 - t)**3 * start_x +
                3 * (1 - t)**2 * t * cp1_x +
                3 * (1 - t) * t**2 * cp2_x +
                t**3 * target_x
            )

            y = (
                (1 - t)**3 * start_y +
                3 * (1 - t)**2 * t * cp1_y +
                3 * (1 - t) * t**2 * cp2_y +
                t**3 * target_y
            )

            points.append((x, y))

        return points

