/** Per-page mouse position tracking. Avoids cross-tab state leaking. */
const mousePositions = new WeakMap();
function getMousePos(page) {
    return mousePositions.get(page) ?? { x: 0, y: 0 };
}
function setMousePos(page, pos) {
    mousePositions.set(page, pos);
}
function rand(min, max) {
    return min + Math.random() * (max - min);
}
function sleep(ms) {
    return new Promise((r) => setTimeout(r, ms));
}
function cubicBezier(t, p0, p1, p2, p3) {
    const mt = 1 - t;
    const mt2 = mt * mt;
    const mt3 = mt2 * mt;
    const t2 = t * t;
    const t3 = t2 * t;
    return {
        x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
        y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y,
    };
}
/**
 * Generate a human-like mouse path using a cubic bezier curve
 * with random control points and sub-pixel jitter.
 */
function generatePath(start, end) {
    const dist = Math.hypot(end.x - start.x, end.y - start.y);
    if (dist < 5)
        return [end];
    const steps = Math.max(8, Math.min(40, Math.floor(dist / 15)));
    const spread = Math.min(dist * 0.2, 150);
    const cp1 = {
        x: start.x + (end.x - start.x) * 0.25 + rand(-spread, spread),
        y: start.y + (end.y - start.y) * 0.25 + rand(-spread, spread),
    };
    const cp2 = {
        x: start.x + (end.x - start.x) * 0.75 + rand(-spread, spread),
        y: start.y + (end.y - start.y) * 0.75 + rand(-spread, spread),
    };
    const points = [];
    for (let i = 1; i <= steps; i++) {
        const t = i / steps;
        const p = cubicBezier(t, start, cp1, cp2, end);
        points.push({
            x: Math.round(p.x + rand(-1, 1)),
            y: Math.round(p.y + rand(-1, 1)),
        });
    }
    // Ensure final point is exact target
    points[points.length - 1] = { x: Math.round(end.x), y: Math.round(end.y) };
    return points;
}
/** Trace one bezier path start→end with slow-fast-slow timing. */
async function tracePath(page, start, end) {
    const path = generatePath(start, end);
    for (let i = 0; i < path.length; i++) {
        await page.mouse.move(path[i].x, path[i].y);
        const progress = i / path.length;
        // Bell curve speed: slow at edges, fast in middle
        const speed = 1 - 4 * (progress - 0.5) ** 2;
        const delay = rand(2, 6) + (1 - speed) * rand(4, 12);
        await sleep(delay);
    }
}
/**
 * Move mouse along a bezier curve with slow-fast-slow timing.
 *
 * For non-trivial distances, ~15% of moves overshoot the target by a few px
 * then make a quick corrective move back — real hands rarely land dead-on.
 */
export async function humanMove(page, targetX, targetY) {
    const start = getMousePos(page);
    const target = { x: targetX, y: targetY };
    const dist = Math.hypot(targetX - start.x, targetY - start.y);
    if (dist > 40 && Math.random() < 0.15) {
        const overshootPx = 4 + Math.random() * 10;
        const ang = Math.atan2(targetY - start.y, targetX - start.x);
        const over = {
            x: Math.round(targetX + Math.cos(ang) * overshootPx),
            y: Math.round(targetY + Math.sin(ang) * overshootPx),
        };
        await tracePath(page, start, over);
        await sleep(rand(40, 90)); // notice the overshoot
        await tracePath(page, over, target); // correct back
    }
    else {
        await tracePath(page, start, target);
    }
    setMousePos(page, target);
}
/**
 * Human-like drag: bezier move to source, mousedown, bezier move to target, pause, mouseup.
 */
export async function humanDrag(page, srcX, srcY, dstX, dstY) {
    await humanMove(page, srcX, srcY);
    await page.mouse.down();
    try {
        await sleep(rand(80, 200));
        await humanMove(page, dstX, dstY);
        await sleep(rand(60, 150));
    }
    finally {
        await page.mouse.up().catch(() => { });
    }
}
/**
 * Type text into the focused element one character at a time with
 * randomized inter-key delays. Uses real CDP key events (isTrusted=true)
 * so React/Vue/Svelte controlled inputs see the changes through their
 * native input handler — no value-tracker workarounds needed.
 *
 * Constant `delay` on Playwright's `keyboard.type(text, { delay })` is
 * uniformly spaced and easy for behavioral fingerprinting to flag; this
 * helper jitters per stroke. The trailing delay is omitted so we don't
 * pay tail latency on the last character.
 */
export async function humanType(page, text) {
    // Array.from handles surrogate pairs / astral characters (emoji etc.)
    const chars = Array.from(text);
    for (let i = 0; i < chars.length; i++) {
        const ch = chars[i];
        // ~2% of keystrokes: fat-finger an adjacent QWERTY key, then backspace and
        // correct — real typing has typos. Only for letters with a known neighbor.
        if (Math.random() < 0.02) {
            const neighbors = QWERTY_NEIGHBORS[ch.toLowerCase()];
            if (neighbors) {
                const wrong = neighbors[Math.floor(Math.random() * neighbors.length)];
                const isUpper = ch !== ch.toLowerCase() && ch === ch.toUpperCase();
                await page.keyboard.type(isUpper ? wrong.toUpperCase() : wrong);
                await sleep(rand(90, 220)); // notice the mistake
                await page.keyboard.press("Backspace");
                await sleep(rand(60, 150));
            }
        }
        await page.keyboard.type(ch);
        if (i < chars.length - 1) {
            await sleep(rand(40, 120));
        }
    }
}
/** Adjacent keys on a QWERTY keyboard, for realistic fat-finger typos. */
const QWERTY_NEIGHBORS = {
    q: "wa", w: "qase", e: "wsdr", r: "edft", t: "rfgy", y: "tghu",
    u: "yhji", i: "ujko", o: "iklp", p: "ol",
    a: "qwsz", s: "awedxz", d: "serfcx", f: "drtgvc", g: "ftyhbv",
    h: "gyujnb", j: "huikmn", k: "jiolm", l: "kop",
    z: "asx", x: "zsdc", c: "xdfv", v: "cfgb", b: "vghn", n: "bhjm", m: "njk",
};
/**
 * Insert text into the currently focused element using whichever path the
 * element type supports best. Caller is responsible for focusing first.
 *
 * Strategy by host type:
 *
 * - `<input>` / `<textarea>`: bypass any framework value-setter override
 *   (React's controlled-component tracker) by writing through the
 *   prototype's native setter, then fire `input`+`change` so onChange
 *   sees the new value.
 *
 * - contenteditable hosts (Draft.js / Lexical / ProseMirror / Tiptap /
 *   Slate / plain CE): send a single CDP `Input.insertText` via
 *   `page.keyboard.insertText`. This is a browser-level trusted
 *   text-input commit — Draft.js's `beforeinput` handler picks it up and
 *   inserts through its EditorState reducer cleanly. The previous
 *   `document.execCommand('insertText', ...)` path triggered a React
 *   reconciliation crash on Draft.js (NotFoundError: removeChild not a
 *   child of this node) which manifested in production as duplicated
 *   text (DOM left in inconsistent state mid-render with both old and
 *   new content present). Confirmed locally with a Playwright + real
 *   Draft.js 0.10.5 + React 16.14 fixture: execCommand left the editor
 *   empty with the React error logged; CDP insertText inserted the text
 *   exactly once with no errors. Same behavior on plain CE and
 *   <input>/<textarea> when tested through the same CDP path.
 *
 * Notes:
 * - Does NOT touch the system clipboard (safe for concurrent dipclaw
 *   instances and the user's own clipboard).
 */
export async function pasteText(page, text) {
    // Probe element type first so we can pick the right insertion path.
    // Throwing from inside page.evaluate would lose the original error
    // context — separate read avoids that.
    const kind = await page.evaluate(() => {
        const el = document.activeElement;
        if (!el)
            return "none";
        const tag = el.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA")
            return "input";
        if (el.isContentEditable || el.getAttribute("role") === "textbox") {
            return "editable";
        }
        return "unsupported:" + tag.toLowerCase();
    });
    if (kind === "none") {
        throw new Error("pasteText: no focused element");
    }
    if (kind.startsWith("unsupported:")) {
        throw new Error(`pasteText: unsupported element <${kind.split(":")[1]}>`);
    }
    if (kind === "input") {
        await page.evaluate((t) => {
            const el = document.activeElement;
            if (!el)
                return;
            const tag = el.tagName;
            const start = el.selectionStart ?? el.value.length;
            const end = el.selectionEnd ?? el.value.length;
            const nextValue = el.value.slice(0, start) + t + el.value.slice(end);
            // React (and other controlled-component frameworks) overrides the
            // `value` property setter on the prototype to maintain an internal
            // value tracker. Assigning `input.value = ...` goes through that
            // wrapper and updates the tracker, so React's onChange interceptor
            // later sees current === tracked and skips the update; the next
            // render then snaps the value back to controlled state.
            // Use the un-overridden native setter so the tracker stays stale and
            // React fires onChange on the dispatched input event below.
            const proto = tag === "INPUT"
                ? HTMLInputElement.prototype
                : HTMLTextAreaElement.prototype;
            const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
            if (nativeSetter) {
                nativeSetter.call(el, nextValue);
            }
            else {
                el.value = nextValue;
            }
            const caret = start + t.length;
            el.setSelectionRange(caret, caret);
            el.dispatchEvent(new InputEvent("input", {
                bubbles: true,
                inputType: "insertFromPaste",
                data: t,
            }));
            el.dispatchEvent(new Event("change", { bubbles: true }));
        }, text);
        return;
    }
    // contenteditable / rich editor path. CDP Input.insertText.
    await page.keyboard.insertText(text);
}
/**
 * Human-like scrolling: multiple small chunks with random intervals.
 */
export async function humanScroll(page, totalDelta) {
    const direction = totalDelta > 0 ? 1 : -1;
    let remaining = Math.abs(totalDelta);
    while (remaining > 0) {
        const chunk = Math.min(remaining, rand(50, 150));
        await page.mouse.wheel(0, chunk * direction);
        remaining -= chunk;
        if (remaining > 0) {
            await sleep(rand(30, 100));
        }
    }
}
//# sourceMappingURL=humanize.js.map