"""
Biomechanics — Organic Thumb Kinematics & Bézier Gesture Synthesis.

Simulates the physical behavior of a human thumb across an entire bot session:
- Spatial drift (posture changes)
- Fatigue (slower, less accurate over time)
- Handedness bias (right-handers arc right)
- Non-linear Bézier touch paths with sigmoid velocity and Gaussian pressure

This module produces gesture data (point sequences) that are then injected
via SendEventInjector or fall back to adb `input swipe`.
"""

import logging
import math
import random
import time

logger = logging.getLogger(__name__)


class PhysicsBody:
    """
    Kinematic model of a human thumb over a session.

    Tracks anchor position (where the thumb naturally rests), session-level
    spatial drift (simulating posture changes), and fatigue (affecting speed
    and accuracy). Provides biomechanically plausible start/end positions
    for all gestures.
    """

    _session_instance = None

    def __init__(self, handedness="right", device_info=None):
        self.handedness = handedness

        # Defensive parsing — device_info may contain MagicMock objects in tests
        try:
            self.w = int(device_info.get("displayWidth", 1080)) if device_info else 1080
        except (TypeError, ValueError):
            self.w = 1080
        try:
            self.h = int(device_info.get("displayHeight", 2400)) if device_info else 2400
        except (TypeError, ValueError):
            self.h = 2400

        # Anchor Point: natural thumb rest position
        # Right-handers: lower-right quadrant; Left-handers: lower-left
        self.anchor_x = self.w * (0.75 if handedness == "right" else 0.25)
        self.anchor_y = self.h * 0.82

        # Session Drift: simulates posture shifts over time
        self.drift_x = 0.0
        self.drift_y = 0.0
        self.gesture_count = 0

        # Fatigue Model: 0.0 = fresh, 1.0 = exhausted
        self.fatigue = 0.0
        self.last_gesture_time = time.time()

        logger.debug(
            f"🦴 [PhysicsBody] Initialized: {handedness}-handed, "
            f"anchor=({self.anchor_x:.0f}, {self.anchor_y:.0f}), "
            f"display={self.w}x{self.h}"
        )

    @classmethod
    def get_session_instance(cls, device=None, handedness="right"):
        """
        Returns a session-persistent PhysicsBody.
        The body persists across all gestures within a single bot session,
        accumulating drift and fatigue realistically.
        """
        if cls._session_instance is None:
            device_info = {}
            if device:
                try:
                    raw = device.get_info()
                    # Defensive: convert to plain dict to handle MagicMock returns
                    if isinstance(raw, dict):
                        device_info = raw
                    else:
                        device_info = {}
                except Exception:
                    pass
            cls._session_instance = cls(handedness=handedness, device_info=device_info)
        return cls._session_instance

    @classmethod
    def reset(cls):
        """Reset for testing / new session."""
        cls._session_instance = None

    def get_scroll_start(self):
        """
        Returns a biomechanically plausible scroll start position.
        Right-handers start scrolls on the right side of the screen,
        with Gaussian jitter and session drift applied.
        """
        self._apply_session_drift()
        self._update_fatigue()

        # Base position: right side for right-handers, avoiding edges
        base_x = self.anchor_x + self.drift_x
        # Scroll starts in the lower 70-85% of the screen
        base_y = self.h * random.uniform(0.70, 0.85) + self.drift_y

        # Gaussian jitter (natural inaccuracy, increases with fatigue)
        fatigue_mult = 1.0 + self.fatigue * 0.5
        jitter_x = random.gauss(0, self.w * 0.02 * fatigue_mult)
        jitter_y = random.gauss(0, self.h * 0.015 * fatigue_mult)

        x = int(max(50, min(self.w - 50, base_x + jitter_x)))
        y = int(max(200, min(self.h - 200, base_y + jitter_y)))
        self.gesture_count += 1
        return x, y

    def get_tap_position(self, target_x, target_y):
        """
        Returns a biomechanically plausible tap position near the target.
        Applies thumb bias (right-handers land slightly left-down of center)
        and Gaussian jitter.
        """
        self._update_fatigue()

        # Thumb bias: right-handers hit slightly left and below center
        bias_x = -3 if self.handedness == "right" else 3
        bias_y = 4  # Thumb pad is below the actual contact center

        fatigue_mult = 1.0 + self.fatigue * 0.3
        jitter_x = random.gauss(bias_x, 5 * fatigue_mult)
        jitter_y = random.gauss(bias_y, 5 * fatigue_mult)

        x = int(max(5, min(self.w - 5, target_x + jitter_x)))
        y = int(max(5, min(self.h - 5, target_y + jitter_y)))
        self.gesture_count += 1
        return x, y

    def get_thumb_arc_bias(self):
        """
        Returns the horizontal arc bias for scroll curves.
        Right-handers naturally arc their thumb to the right during
        vertical swipes; left-handers arc left.
        """
        base_arc = self.w * 0.04
        if self.handedness == "right":
            return base_arc + random.uniform(-self.w * 0.01, self.w * 0.02)
        else:
            return -(base_arc + random.uniform(-self.w * 0.01, self.w * 0.02))

    def get_pressure_baseline(self):
        """
        Returns the baseline pressure for touch events.
        Fatigued thumbs press harder (compensating for reduced precision).
        """
        baseline = 0.35 + self.fatigue * 0.15
        return min(0.85, baseline + random.uniform(-0.05, 0.05))

    def get_touch_major(self):
        """
        Returns the touch contact area (touch_major) in device units.
        Fatigued thumbs have a larger contact patch (flatter press).
        """
        base = 6 + int(self.fatigue * 4)
        return max(4, base + random.randint(-2, 2))

    def _apply_session_drift(self):
        """
        Every ~15-25 gestures, apply a small posture shift.
        Simulates the user adjusting their grip on the phone.
        """
        drift_interval = random.randint(15, 25)
        if self.gesture_count > 0 and self.gesture_count % drift_interval == 0:
            old_dx, old_dy = self.drift_x, self.drift_y
            self.drift_x += random.gauss(0, self.w * 0.025)
            self.drift_y += random.gauss(0, self.h * 0.015)

            # Clamp drift so we don't wander off the screen
            self.drift_x = max(-self.w * 0.1, min(self.w * 0.1, self.drift_x))
            self.drift_y = max(-self.h * 0.06, min(self.h * 0.06, self.drift_y))

            if abs(self.drift_x - old_dx) > 5 or abs(self.drift_y - old_dy) > 5:
                logger.debug(
                    f"🦴 [PhysicsBody] Posture drift: "
                    f"Δx={self.drift_x - old_dx:+.0f}, Δy={self.drift_y - old_dy:+.0f} "
                    f"(gesture #{self.gesture_count})"
                )

    def _update_fatigue(self):
        """
        Update fatigue based on gesture frequency.
        Rapid gestures increase fatigue; idle periods recover it.
        """
        elapsed = time.time() - self.last_gesture_time
        if elapsed < 0.5:
            # Rapid-fire: fatigue increases
            self.fatigue = min(1.0, self.fatigue + 0.015)
        elif elapsed > 8.0:
            # Long pause: recovery
            self.fatigue = max(0.0, self.fatigue - 0.08)
        elif elapsed > 3.0:
            # Moderate pause: slight recovery
            self.fatigue = max(0.0, self.fatigue - 0.02)
        self.last_gesture_time = time.time()


class BezierGesture:
    """
    Generates multi-point cubic Bézier curves for organic touch paths.

    Replaces the linear A→B interpolation of `adb shell input swipe`
    with biomechanically accurate gesture trajectories including:
    - Thumb arc curvature (handedness-dependent)
    - Sigmoid velocity profile (slow start → fast middle → slow end)
    - Gaussian pressure curve (light touch → firm contact → light lift)
    """

    @staticmethod
    def scroll_curve(start, end, body: PhysicsBody, n_points=None):
        """
        Generates a vertical scroll gesture curve.

        Args:
            start: (x, y) start position
            end: (x, y) end position
            body: PhysicsBody for handedness/fatigue context
            n_points: Override for number of intermediate points

        Returns:
            List of (x, y, pressure) tuples along the Bézier curve
        """
        sx, sy = start
        ex, ey = end

        if n_points is None:
            n_points = random.randint(10, 18)

        # Thumb arc: the control points bias the curve sideways
        arc_bias = body.get_thumb_arc_bias()

        # Two control points for cubic Bézier
        # CP1: early in the gesture, slight arc
        cp1_x = sx + arc_bias * random.uniform(0.2, 0.4)
        cp1_y = sy + (ey - sy) * random.uniform(0.2, 0.35)
        # CP2: later in the gesture, peak arc
        cp2_x = sx + arc_bias * random.uniform(0.5, 0.8)
        cp2_y = sy + (ey - sy) * random.uniform(0.65, 0.8)

        pressure_baseline = body.get_pressure_baseline()

        points = []
        for i in range(n_points + 1):
            t = i / n_points

            # Cubic Bézier interpolation
            x = (1 - t) ** 3 * sx + 3 * (1 - t) ** 2 * t * cp1_x + 3 * (1 - t) * t**2 * cp2_x + t**3 * ex
            y = (1 - t) ** 3 * sy + 3 * (1 - t) ** 2 * t * cp1_y + 3 * (1 - t) * t**2 * cp2_y + t**3 * ey

            # Micro-noise on each point (finger vibration)
            x += random.gauss(0, 1.5)
            y += random.gauss(0, 1.5)

            # Pressure curve: Gaussian peak around t=0.4 (peak contact mid-gesture)
            pressure = pressure_baseline + 0.3 * math.exp(-((t - 0.4) ** 2) / 0.1)
            pressure += random.uniform(-0.04, 0.04)
            pressure = max(0.08, min(0.92, pressure))

            points.append((int(x), int(y), round(pressure, 3)))

        return points

    @staticmethod
    def tap_curve(target_x, target_y, body: PhysicsBody):
        """
        Generates a tap gesture (touch-down → micro-drift → touch-up).

        Returns:
            List of (x, y, pressure) tuples (typically 3-5 points)
        """
        tx, ty = body.get_tap_position(target_x, target_y)
        pressure_base = body.get_pressure_baseline()

        # Touch-down (initial light contact)
        p_down = max(0.1, pressure_base * 0.6 + random.uniform(-0.05, 0.05))
        # Full contact
        p_full = min(0.9, pressure_base + random.uniform(-0.05, 0.1))
        # Release
        p_up = max(0.05, pressure_base * 0.3 + random.uniform(-0.03, 0.03))

        # Micro-drift: finger slides ~2-6px during contact
        drift_x = random.randint(-4, 4)
        drift_y = random.randint(-4, 4)

        points = [
            (tx, ty, round(p_down, 3)),
            (tx + drift_x // 2, ty + drift_y // 2, round(p_full, 3)),
            (tx + drift_x, ty + drift_y, round(p_up, 3)),
        ]
        return points

    @staticmethod
    def horizontal_swipe_curve(start, end, body: PhysicsBody, n_points=None):
        """
        Generates a horizontal swipe curve (e.g., carousel browsing).

        Includes vertical arc (thumb drops downward when swiping left-to-right
        for right-handers) and sigmoid velocity.
        """
        sx, sy = start
        ex, ey = end

        if n_points is None:
            n_points = random.randint(8, 14)

        # Vertical arc for horizontal swipes
        # Right-handers swiping left: thumb drops 30-90px
        direction = 1 if ex < sx else -1  # 1 = swiping left
        if body.handedness == "right":
            y_arc = direction * random.uniform(25, 70)
        else:
            y_arc = -direction * random.uniform(25, 70)

        # Control points
        cp1_x = sx + (ex - sx) * random.uniform(0.25, 0.35)
        cp1_y = sy + y_arc * 0.4
        cp2_x = sx + (ex - sx) * random.uniform(0.65, 0.75)
        cp2_y = sy + y_arc * 0.9

        pressure_baseline = body.get_pressure_baseline()
        points = []

        for i in range(n_points + 1):
            t = i / n_points
            x = (1 - t) ** 3 * sx + 3 * (1 - t) ** 2 * t * cp1_x + 3 * (1 - t) * t**2 * cp2_x + t**3 * ex
            y = (1 - t) ** 3 * sy + 3 * (1 - t) ** 2 * t * cp1_y + 3 * (1 - t) * t**2 * cp2_y + t**3 * ey
            x += random.gauss(0, 2)
            y += random.gauss(0, 2)

            pressure = pressure_baseline + 0.25 * math.exp(-((t - 0.45) ** 2) / 0.12)
            pressure += random.uniform(-0.04, 0.04)
            pressure = max(0.08, min(0.92, pressure))

            points.append((int(x), int(y), round(pressure, 3)))

        return points

    @staticmethod
    def compute_sigmoid_timing(n_points, total_duration_ms):
        """
        Generates a sigmoid-based timing schedule for gesture points.

        Produces intervals that are longer at the start and end
        (slow acceleration/deceleration) and shorter in the middle
        (peak velocity). This matches real human swipe kinematics.

        Returns:
            List of inter-point delay times in seconds (length n_points)
        """
        if n_points <= 1:
            return [total_duration_ms / 1000.0]

        # Generate sigmoid-spaced t values
        raw_intervals = []
        for i in range(n_points):
            # Normalized position
            t = i / (n_points - 1) if n_points > 1 else 0.5
            # Inverted sigmoid: fast in middle, slow at edges
            # Higher value = longer delay = slower movement
            1.0 / (1.0 + math.exp(-8 * (t - 0.5)))
            # U-shaped: slow at start & end, fast in middle
            speed_factor = 0.4 + 1.2 * (4 * (t - 0.5) ** 2)
            raw_intervals.append(speed_factor)

        # Normalize to total duration
        total_raw = sum(raw_intervals)
        total_sec = total_duration_ms / 1000.0
        intervals = [(r / total_raw) * total_sec for r in raw_intervals]

        # Add micro-jitter to timing (humans are never perfectly rhythmic)
        intervals = [max(0.002, i + random.uniform(-0.003, 0.003)) for i in intervals]

        return intervals

    @staticmethod
    def compute_fling_timing(n_points, total_duration_ms):
        """
        Generates a J-curve timing schedule for flick/swipe gestures.

        Unlike the sigmoid (which slows down at the end), this curve
        accelerates through the middle and maintains high velocity
        until the very last point to simulate a sudden 'liftoff' flick.
        This allows Android's ScrollView to register a high fling velocity.

        Returns:
            List of inter-point delay times in seconds (length n_points)
        """
        if n_points <= 1:
            return [total_duration_ms / 1000.0]

        raw_intervals = []
        for i in range(n_points):
            t = i / (n_points - 1)
            # Starts slow (larger delay), speeds up continuously (smaller delay)
            speed_factor = 1.0 - (0.8 * t)
            raw_intervals.append(speed_factor)

        total_raw = sum(raw_intervals)
        total_sec = total_duration_ms / 1000.0
        intervals = [(r / total_raw) * total_sec for r in raw_intervals]

        # Add micro-jitter to timing
        intervals = [max(0.002, i + random.uniform(-0.003, 0.003)) for i in intervals]

        return intervals
