/**
 * Human-like behaviour primitives — timing, scrolling, clicking.
 *
 * Every delay is drawn from a gaussian distribution so patterns
 * are not trivially detectable.  A per-session jitter factor
 * shifts *all* timing parameters by ±20% for the lifetime of the
 * import.
 */

import { randomBytes } from 'node:crypto';
import type { BrowserContext } from './cdp.js';
import { config } from './config.js';

// ── Per-session jitter (computed once at import time) ──────────────────────

/** Deterministic float in [0, 1) from crypto bytes. */
function cryptoRandom(): number {
  return randomBytes(4).readUInt32BE(0) / 0x1_0000_0000;
}

/** Session-wide multiplier in [1 − jitter, 1 + jitter]. */
const SESSION_FACTOR =
  1 + (cryptoRandom() * 2 - 1) * config.timing.sessionJitter;

function jitter(ms: number): number {
  return ms * SESSION_FACTOR;
}

// ── Gaussian random ────────────────────────────────────────────────────────

/**
 * Box-Muller transform — returns a sample from N(mean, sigma²).
 * Result is clamped to [mean − 3σ, mean + 3σ] to avoid extreme outliers.
 */
function gaussianRandom(mean: number, sigma: number): number {
  let u1 = 0;
  let u2 = 0;
  while (u1 === 0) u1 = cryptoRandom();
  while (u2 === 0) u2 = cryptoRandom();
  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  const raw = mean + z * sigma;
  return Math.max(mean - 3 * sigma, Math.min(mean + 3 * sigma, raw));
}

// ── Public API ─────────────────────────────────────────────────────────────

/**
 * Pause for a gaussian-distributed duration.
 * @param meanMs  Centre of the distribution (ms)
 * @param sigmaMs Standard deviation (default: meanMs × 0.3)
 */
export function delay(meanMs: number, sigmaMs?: number): Promise<void> {
  const sigma = sigmaMs ?? meanMs * 0.3;
  const ms = Math.max(0, jitter(gaussianRandom(meanMs, sigma)));
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Scroll the page by approximately `pixels` using multiple wheel events
 * with natural speed variation.
 */
export async function naturalScroll(
  ctx: BrowserContext,
  pixels: number,
): Promise<void> {
  const direction = pixels > 0 ? 1 : -1;
  let remaining = Math.abs(pixels);

  while (remaining > 0) {
    const chunk = Math.min(
      remaining,
      gaussianRandom(120, 30) * direction > 0 ? gaussianRandom(120, 30) : 120,
    );
    const deltaY = Math.round(Math.min(chunk, remaining)) * direction;

    await ctx.Input.dispatchMouseEvent({
      type: 'mouseWheel',
      x: Math.round(gaussianRandom(400, 80)),
      y: Math.round(gaussianRandom(400, 80)),
      deltaX: 0,
      deltaY,
    });

    remaining -= Math.abs(deltaY);
    await delay(gaussianRandom(30, 10));
  }
}

/**
 * Click at (x, y) with a slight random offset.
 */
export async function humanClick(
  ctx: BrowserContext,
  x: number,
  y: number,
  offsetPx: number = config.timing.clickOffset,
): Promise<void> {
  const cx = x + gaussianRandom(0, offsetPx);
  const cy = y + gaussianRandom(0, offsetPx);

  await ctx.Input.dispatchMouseEvent({ type: 'mouseMoved', x: cx, y: cy });
  await delay(40, 15);
  await ctx.Input.dispatchMouseEvent({
    type: 'mousePressed',
    x: cx,
    y: cy,
    button: 'left',
    clickCount: 1,
  });
  await delay(70, 20);
  await ctx.Input.dispatchMouseEvent({
    type: 'mouseReleased',
    x: cx,
    y: cy,
    button: 'left',
    clickCount: 1,
  });
}

/**
 * Click on a DOM element identified by CSS selector.
 * Returns false if the element was not found.
 */
export async function clickElement(
  ctx: BrowserContext,
  selector: string,
): Promise<boolean> {
  const nodeId = await ctx.querySelector(selector);
  if (nodeId === null) return false;

  const box = await ctx.getBoxModel(nodeId);
  if (!box) return false;

  const cx = box.x + box.width / 2;
  const cy = box.y + box.height / 2;

  await mouseMoveTo(ctx, cx, cy);
  await delay(80, 25);
  await humanClick(ctx, cx, cy);
  return true;
}

/**
 * Move the mouse from its current position to (x, y) along an
 * interpolated path with slight curvature.
 */
export async function mouseMoveTo(
  ctx: BrowserContext,
  x: number,
  y: number,
): Promise<void> {
  const steps = Math.round(gaussianRandom(12, 3));
  const safeSteps = Math.max(steps, 3);

  // We don't know the current position, so start from a reasonable default.
  // Intermediate points add slight curve via gaussian offsets.
  for (let i = 1; i <= safeSteps; i++) {
    const t = i / safeSteps;
    const curveX = gaussianRandom(0, 2);
    const curveY = gaussianRandom(0, 2);
    await ctx.Input.dispatchMouseEvent({
      type: 'mouseMoved',
      x: x * t + curveX,
      y: y * t + curveY,
    });
    await delay(gaussianRandom(12, 4));
  }
}

/**
 * Longer pause that simulates reading or thinking (5–15 s).
 */
export async function idlePause(): Promise<void> {
  const { min, max } = config.timing.idlePause;
  const ms = jitter(min + cryptoRandom() * (max - min));
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Incrementally scroll a feed, invoking `onScroll` after each step.
 * The callback receives the current `scrollY` and may return `false`
 * to stop scrolling early.
 */
export async function scrollFeed(
  ctx: BrowserContext,
  opts: {
    maxScrolls?: number;
    onScroll?: (scrollY: number) => Promise<boolean>;
  } = {},
): Promise<void> {
  const maxScrolls = opts.maxScrolls ?? config.scroll.maxScrolls;

  for (let i = 0; i < maxScrolls; i++) {
    const step =
      config.scroll.stepMin +
      cryptoRandom() * (config.scroll.stepMax - config.scroll.stepMin);

    await naturalScroll(ctx, Math.round(step));

    // Wait for content to load / simulate reading
    await delay(
      config.timing.scrollPause.mean,
      config.timing.scrollPause.sigma,
    );

    if (opts.onScroll) {
      const scrollY = await ctx.evaluate<number>(
        'window.scrollY',
      );
      const shouldContinue = await opts.onScroll(scrollY);
      if (shouldContinue === false) break;
    }
  }
}
