package browser

import (
	"fmt"
	"math"
	"math/rand"
	"time"
)

// MouseHumanOpts configura el movimiento humano del ratón.
type MouseHumanOpts struct {
	// Mode es la política de velocidad: "auto"/"fast" (rápido), "human" (sigiloso,
	// también "") o "instant". Controla los defaults de Steps/DurationMs/JitterPx y
	// la pausa press/release:
	//   - auto/fast: recta ~5 pts, 40-80ms, jitter mínimo (eventos de ratón reales,
	//              rápido — modo por defecto del MCP para automatización propia).
	//   - human:   Bézier ~25 pts, 350-800ms, jitter 2px (sigilo anti-bot alto).
	//   - instant: sin movimiento de ratón (CdpMoveMouseHuman es no-op); el click
	//              por #ref usa element.click() JS. Para tests y fallback sin bbox.
	// Los valores explícitos (Steps/DurationMs/JitterPx != 0) ganan al preset del modo.
	Mode string
	// Steps es el número de puntos intermedios de la curva (default según Mode).
	Steps int
	// DurationMs es la duración total aproximada del movimiento en milisegundos.
	// Si es 0, se elige según Mode.
	DurationMs int
	// JitterPx es la desviación perpendicular máxima por punto en píxeles (default según Mode).
	JitterPx float64
	// FromX es la coordenada X de origen. Si < 0, se usa (0, 0) como origen.
	FromX float64
	// FromY es la coordenada Y de origen. Si < 0, se usa (0, 0) como origen.
	FromY float64
}

// MouseProfileForMode construye las opciones de ratón para un modo de velocidad.
// Es la fuente única que MCP, runner YAML y CLI usan para mapear un modo a opts,
// sin duplicar números. El mapeo modo→valores concretos vive en mouseHumanDefaults.
// Un modo desconocido se trata como "human" (el más seguro).
func MouseProfileForMode(mode string) MouseHumanOpts {
	switch mode {
	case "auto", "fast", "instant", "human", "":
		return MouseHumanOpts{Mode: mode, FromX: -1, FromY: -1}
	default:
		return MouseHumanOpts{Mode: "human", FromX: -1, FromY: -1}
	}
}

// mouseHumanDefaults aplica valores por defecto a opts según opts.Mode.
func mouseHumanDefaults(opts MouseHumanOpts) MouseHumanOpts {
	switch opts.Mode {
	case "instant":
		// El movimiento se omite en CdpMoveMouseHuman; valores mínimos por si acaso.
		if opts.Steps <= 0 {
			opts.Steps = 1
		}
		if opts.DurationMs <= 0 {
			opts.DurationMs = 1
		}
		// JitterPx se queda en 0.
	case "fast", "auto":
		if opts.Steps <= 0 {
			opts.Steps = 5
		}
		if opts.DurationMs <= 0 {
			opts.DurationMs = 40 + rand.Intn(41) // 40..80
		}
		// JitterPx se queda en lo recibido (0 por defecto, sin jitter en fast/auto).
	default: // "human" o ""
		if opts.Steps <= 0 {
			opts.Steps = 25
		}
		if opts.DurationMs <= 0 {
			opts.DurationMs = 350 + rand.Intn(451) // 350..800
		}
		if opts.JitterPx <= 0 {
			opts.JitterPx = 2.0
		}
	}
	if opts.FromX < 0 {
		opts.FromX = 0
	}
	if opts.FromY < 0 {
		opts.FromY = 0
	}
	return opts
}

// smoothstep aplica easing suave (ease-in-out) al parámetro t ∈ [0,1].
// Produce aceleración inicial y desaceleración final, imitando movimiento humano.
func smoothstep(t float64) float64 {
	return t * t * (3 - 2*t)
}

// bezierPoint evalúa la curva de Bézier cúbica en el parámetro t ∈ [0,1].
// p0 = origen, p1/p2 = puntos de control, p3 = destino.
func bezierPoint(p0, p1, p2, p3 [2]float64, t float64) [2]float64 {
	u := 1 - t
	u2 := u * u
	u3 := u2 * u
	t2 := t * t
	t3 := t2 * t
	return [2]float64{
		u3*p0[0] + 3*u2*t*p1[0] + 3*u*t2*p2[0] + t3*p3[0],
		u3*p0[1] + 3*u2*t*p1[1] + 3*u*t2*p2[1] + t3*p3[1],
	}
}

// bezierPath genera los puntos de una curva de Bézier cúbica desde p0 hasta p3
// usando los puntos de control ctrl1 y ctrl2. Retorna steps+1 puntos
// (incluye origen y destino). Esta función es pura y testeable sin Chrome.
func bezierPath(p0, p3, ctrl1, ctrl2 [2]float64, steps int) [][2]float64 {
	if steps < 1 {
		steps = 1
	}
	pts := make([][2]float64, steps+1)
	for i := 0; i <= steps; i++ {
		t := smoothstep(float64(i) / float64(steps))
		pts[i] = bezierPoint(p0, ctrl1, ctrl2, p3, t)
	}
	return pts
}

// randomControlPoints genera dos puntos de control aleatorios desplazados
// lateralmente del segmento recto p0→p3, produciendo el arco curvo humano.
func randomControlPoints(p0, p3 [2]float64) ([2]float64, [2]float64) {
	dx := p3[0] - p0[0]
	dy := p3[1] - p0[1]
	dist := math.Sqrt(dx*dx + dy*dy)
	if dist < 1 {
		dist = 1
	}

	// Vector perpendicular unitario al segmento
	perpX := -dy / dist
	perpY := dx / dist

	// Desplazamiento lateral: entre 10% y 40% de la distancia total
	lat1 := dist * (0.1 + rand.Float64()*0.3) * (1 - 2*float64(rand.Intn(2)))
	lat2 := dist * (0.1 + rand.Float64()*0.3) * (1 - 2*float64(rand.Intn(2)))

	// Puntos de control en 1/3 y 2/3 del segmento + desplazamiento lateral
	ctrl1 := [2]float64{
		p0[0] + dx/3 + perpX*lat1,
		p0[1] + dy/3 + perpY*lat1,
	}
	ctrl2 := [2]float64{
		p0[0] + 2*dx/3 + perpX*lat2,
		p0[1] + 2*dy/3 + perpY*lat2,
	}
	return ctrl1, ctrl2
}

// CdpMoveMouseHuman mueve el ratón desde (opts.FromX, opts.FromY) hasta (toX, toY)
// siguiendo una trayectoria de Bézier cúbica con easing suave y micro-jitter,
// imitando el movimiento humano para reducir la detección de automatización.
//
// Despacha Input.dispatchMouseEvent {type:"mouseMoved"} en cada punto de la curva
// con pausas proporcionales a DurationMs/Steps (±20% de variación aleatoria).
func CdpMoveMouseHuman(c *CDPConn, toX, toY float64, opts MouseHumanOpts) error {
	if c == nil {
		return fmt.Errorf("cdp move mouse human: conexion nula")
	}
	opts = mouseHumanDefaults(opts)

	// Modo instant: sin movimiento de ratón (el click lo resuelve quien llama,
	// por coords directas o por element.click() JS).
	if opts.Mode == "instant" {
		return nil
	}

	p0 := [2]float64{opts.FromX, opts.FromY}
	p3 := [2]float64{toX, toY}
	ctrl1, ctrl2 := randomControlPoints(p0, p3)

	pts := bezierPath(p0, p3, ctrl1, ctrl2, opts.Steps)

	// Pausa base por paso en microsegundos
	baseStepUs := int64(opts.DurationMs) * 1000 / int64(opts.Steps)

	// Vector perpendicular al segmento global para el jitter
	dx := toX - opts.FromX
	dy := toY - opts.FromY
	dist := math.Sqrt(dx*dx + dy*dy)
	if dist < 1 {
		dist = 1
	}
	perpX := -dy / dist
	perpY := dx / dist

	for _, pt := range pts {
		// Micro-jitter perpendicular aleatorio
		jitter := (rand.Float64()*2 - 1) * opts.JitterPx
		x := pt[0] + perpX*jitter
		y := pt[1] + perpY*jitter

		if _, err := c.sendCDP("Input.dispatchMouseEvent", map[string]any{
			"type": "mouseMoved",
			"x":    x,
			"y":    y,
		}); err != nil {
			return fmt.Errorf("cdp move mouse human: mouseMoved: %w", err)
		}

		// Pausa con variación ±20%
		variation := int64(float64(baseStepUs) * (0.8 + rand.Float64()*0.4))
		time.Sleep(time.Duration(variation) * time.Microsecond)
	}

	return nil
}
