/**
 * Human-like behavioral layer for cloakbrowser (JS/TS).
 *
 * Activated via humanize: true in launch() / launchContext().
 * Patches page methods to use Bezier mouse curves, realistic typing, and smooth scrolling.
 *
 * Stealth-aware (fixes #110):
 *   - isInputElement / isSelectorFocused use CDP Isolated Worlds instead of page.evaluate
 *   - Shift symbol typing uses CDP Input.dispatchKeyEvent for isTrusted=true events
 *   - Falls back to page.evaluate only when CDP session is unavailable
 *
 * Patches all interaction methods:
 * click, dblclick, hover, type, fill, check, uncheck, selectOption,
 * press, pressSequentially, tap, dragTo, clear + Frame-level equivalents.
 *
 * ELEMENTHANDLE-LEVEL:
 *   click, dblclick, hover, type, fill, press, selectOption,
 *   check, uncheck, setChecked, tap, focus
 *   + $, $$, waitForSelector (nested elements are also patched)
 *
 * page.$(), page.$$(), page.waitForSelector() and Frame equivalents
 * return patched ElementHandles automatically.
 */

import type { Browser, BrowserContext, Page, Frame, CDPSession } from 'playwright-core';
import { HumanConfig, HumanActionOptions, resolveConfig, mergeConfig, rand, randRange, sleep } from './config.js';
import { RawMouse, RawKeyboard, humanMove, humanClick, clickTarget, humanIdle } from './mouse.js';
import { humanType } from './keyboard.js';
import { scrollToElement, humanScrollIntoView } from './scroll.js';
import { patchPageElementHandles, patchFrameElementHandles, patchSingleElementHandle } from './elementhandle.js';
import {
  ensureActionable, ensureStable, checkPointerEvents,
  CHECKS_CLICK, CHECKS_HOVER, CHECKS_INPUT, CHECKS_FOCUS, CHECKS_CHECK,
  type CheckName,
} from './actionability.js';

export { HumanConfig, resolveConfig, mergeConfig } from './config.js';
export { humanMove, humanClick, clickTarget, humanIdle } from './mouse.js';
export { humanType } from './keyboard.js';
export { scrollToElement, humanScrollIntoView } from './scroll.js';
export { patchSingleElementHandle } from './elementhandle.js';

// --- Platform-aware select-all shortcut (macOS uses Meta, others use Control) ---
const SELECT_ALL = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';


// ============================================================================
// CDP Isolated World — stealth DOM evaluation
// ============================================================================

/**
 * Manages a CDP isolated execution context for DOM reads.
 * Produces clean Error.stack traces (no 'eval at evaluate :302:')
 * and is invisible to querySelector monkey-patches in the main world.
 *
 * Context ID is invalidated on navigation and auto-recreated on next call.
 */
class StealthEval {
  private cdp: CDPSession | null = null;
  private contextId: number | null = null;
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  private async ensureCdp(): Promise<CDPSession> {
    if (!this.cdp) {
      this.cdp = await this.page.context().newCDPSession(this.page);
    }
    return this.cdp;
  }

  private async createWorld(): Promise<number> {
    const cdp = await this.ensureCdp();
    const tree = await cdp.send('Page.getFrameTree');
    const frameId = tree.frameTree.frame.id;
    const result = await cdp.send('Page.createIsolatedWorld', {
      frameId,
      worldName: '',
      grantUniveralAccess: true,
    });
    const ctxId = result.executionContextId;
    this.contextId = ctxId;
    return ctxId;
  }

  /**
   * Evaluate a JS expression in the isolated world.
   * Auto-recreates the world if the context was invalidated (navigation).
   * Returns the result value, or undefined on failure.
   */
  async evaluate(expression: string): Promise<any> {
    if (this.contextId === null) {
      await this.createWorld();
    }

    for (let attempt = 0; attempt < 2; attempt++) {
      try {
        const cdp = await this.ensureCdp();
        const result = await cdp.send('Runtime.evaluate', {
          expression,
          contextId: this.contextId!,
          returnByValue: true,
        });

        if (result.exceptionDetails) {
          // Context was likely invalidated by navigation
          if (attempt === 0) {
            await this.createWorld();
            continue;
          }
          return undefined;
        }

        return result.result?.value;
      } catch {
        if (attempt === 0) {
          this.contextId = null;
          try {
            await this.createWorld();
          } catch {
            return undefined;
          }
          continue;
        }
        return undefined;
      }
    }
    return undefined;
  }

  /** Mark context as stale — call after navigation. */
  invalidate(): void {
    this.contextId = null;
  }

  /** Get the underlying CDP session (reused for Input.dispatchKeyEvent etc.). */
  async getCdpSession(): Promise<CDPSession> {
    return this.ensureCdp();
  }
}


// ============================================================================
// Cursor state
// ============================================================================

class CursorState {
  x = 0;
  y = 0;
  initialized = false;
}


// ============================================================================
// Stealth DOM queries — isolated world with evaluate fallback
// ============================================================================

/**
 * Check if selector matches an input/textarea/contenteditable element.
 * Uses CDP Isolated World when available — invisible to main world.
 */
async function isInputElement(
  stealth: StealthEval | null,
  page: Page,
  selector: string,
): Promise<boolean> {
  if (stealth) {
    try {
      const escaped = JSON.stringify(selector);
      const result = await stealth.evaluate(`
        (() => {
          const el = document.querySelector(${escaped});
          if (!el) return false;
          const tag = el.tagName.toLowerCase();
          return tag === 'input' || tag === 'textarea'
            || el.getAttribute('contenteditable') === 'true';
        })()
      `);
      return !!result;
    } catch {
      // Fall through to page.evaluate
    }
  }

  // Fallback: page.evaluate (detectable — should only happen if CDP fails)
  return page.evaluate((sel: string) => {
    const el = document.querySelector(sel);
    if (!el) return false;
    const tag = el.tagName.toLowerCase();
    return tag === 'input' || tag === 'textarea'
      || el.getAttribute('contenteditable') === 'true';
  }, selector).catch(() => false);
}

/**
 * Check if the element matching selector is currently focused.
 * Uses CDP Isolated World when available — invisible to main world.
 */
async function isSelectorFocused(
  stealth: StealthEval | null,
  page: Page,
  selector: string,
): Promise<boolean> {
  if (stealth) {
    try {
      const escaped = JSON.stringify(selector);
      const result = await stealth.evaluate(`
        (() => {
          const el = document.querySelector(${escaped});
          return el === document.activeElement;
        })()
      `);
      return !!result;
    } catch {
      // Fall through to page.evaluate
    }
  }

  return page.evaluate((sel: string) => {
    const el = document.querySelector(sel);
    return el === document.activeElement;
  }, selector).catch(() => false);
}


// ============================================================================
// Page-level patching
// ============================================================================

/**
 * Replace page methods with human-like implementations.
 */
function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
  const originals = {
    click: page.click.bind(page),
    dblclick: page.dblclick.bind(page),
    hover: page.hover.bind(page),
    type: page.type.bind(page),
    fill: page.fill.bind(page),
    check: page.check.bind(page),
    uncheck: page.uncheck.bind(page),
    selectOption: page.selectOption.bind(page),
    press: page.press.bind(page),
    goto: page.goto.bind(page),
    isChecked: page.isChecked.bind(page),
    mouseMove: page.mouse.move.bind(page.mouse),
    mouseClick: page.mouse.click.bind(page.mouse),
    mouseDblclick: page.mouse.dblclick.bind(page.mouse),
    mouseWheel: page.mouse.wheel.bind(page.mouse),
    mouseDown: page.mouse.down.bind(page.mouse),
    mouseUp: page.mouse.up.bind(page.mouse),
    keyboardType: page.keyboard.type.bind(page.keyboard),
    keyboardDown: page.keyboard.down.bind(page.keyboard),
    keyboardUp: page.keyboard.up.bind(page.keyboard),
    keyboardPress: page.keyboard.press.bind(page.keyboard),
    keyboardInsertText: page.keyboard.insertText.bind(page.keyboard),
  };

  (page as any)._original = originals;
  (page as any)._humanCfg = cfg;

  // --- Stealth infrastructure ---
  const stealth = new StealthEval(page);
  (page as any)._stealth = stealth;

  // CDP session for shift symbol typing (lazy-initialized, reuses stealth's session)
  let cdpSession: CDPSession | null = null;
  const ensureCdp = async (): Promise<CDPSession | null> => {
    if (!cdpSession) {
      try {
        cdpSession = await stealth.getCdpSession();
      } catch {}
    }
    return cdpSession;
  };

  const raw: RawMouse = {
    move: originals.mouseMove,
    down: originals.mouseDown,
    up: originals.mouseUp,
    wheel: originals.mouseWheel,
  };

  const rawKb: RawKeyboard = {
    down: originals.keyboardDown,
    up: originals.keyboardUp,
    type: originals.keyboardType,
    insertText: originals.keyboardInsertText,
  };

  async function ensureCursorInit(): Promise<void> {
    if (!cursor.initialized) {
      cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
      cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
      await originals.mouseMove(cursor.x, cursor.y);
      cursor.initialized = true;
    }
  }

  // --- goto (invalidate isolated world on navigation) ---
  const humanGoto = async (url: string, options?: {
    referer?: string;
    timeout?: number;
    waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
  }) => {
    const response = await originals.goto(url, options);
    stealth.invalidate();
    patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
    return response;
  };

  // --- click ---
  const humanClickFn = async (selector: string, options?: HumanActionOptions & { _skipChecks?: boolean }) => {
    await ensureCursorInit();
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const skipChecks = (options as any)?._skipChecks ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force && !skipChecks) {
      await ensureActionable(page, selector, CHECKS_CLICK, remainingMs(), force);
    }
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }
    const { box, cursorX, cursorY, didScroll } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, remainingMs());
    cursor.x = cursorX;
    cursor.y = cursorY;
    const isInput = await isInputElement(stealth, page, selector);
    let finalBox = box;
    if (!force && didScroll) {
      await ensureStable(page, selector, remainingMs());
      finalBox = await page.locator(selector).first().boundingBox({ timeout: Math.max(1, remainingMs()) }) ?? box;
    }
    const target = clickTarget(finalBox, isInput, callCfg);
    if (!force) {
      await checkPointerEvents(page, selector, target.x, target.y, stealth, remainingMs());
    }
    await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
    cursor.x = target.x;
    cursor.y = target.y;
    await humanClick(raw, isInput, callCfg);
  };

  // --- dblclick ---
  const humanDblclickFn = async (selector: string, options?: HumanActionOptions) => {
    await ensureCursorInit();
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_CLICK, remainingMs(), force);
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }
    const { box, cursorX, cursorY, didScroll } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, remainingMs());
    cursor.x = cursorX;
    cursor.y = cursorY;
    const isInput = await isInputElement(stealth, page, selector);
    let finalBox = box;
    if (!force && didScroll) {
      await ensureStable(page, selector, remainingMs());
      finalBox = await page.locator(selector).first().boundingBox({ timeout: Math.max(1, remainingMs()) }) ?? box;
    }
    const target = clickTarget(finalBox, isInput, callCfg);
    if (!force) {
      await checkPointerEvents(page, selector, target.x, target.y, stealth, remainingMs());
    }
    await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
    cursor.x = target.x;
    cursor.y = target.y;
    await raw.down({ clickCount: 2 });
    await sleep(rand(30, 60));
    await raw.up({ clickCount: 2 });
  };

  // --- hover ---
  const humanHoverFn = async (selector: string, options?: HumanActionOptions & { _skipChecks?: boolean }) => {
    await ensureCursorInit();
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const skipChecks = (options as any)?._skipChecks ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force && !skipChecks) await ensureActionable(page, selector, CHECKS_HOVER, remainingMs(), force);
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }
    const { box, cursorX, cursorY, didScroll } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, remainingMs());
    cursor.x = cursorX;
    cursor.y = cursorY;
    let finalBox = box;
    if (!force && didScroll) {
      await ensureStable(page, selector, remainingMs());
      finalBox = await page.locator(selector).first().boundingBox({ timeout: Math.max(1, remainingMs()) }) ?? box;
    }
    const target = clickTarget(finalBox, false, callCfg);
    if (!force) {
      await checkPointerEvents(page, selector, target.x, target.y, stealth, remainingMs());
    }
    await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
    cursor.x = target.x;
    cursor.y = target.y;
  };

  // --- type ---
  const humanTypeFn = async (selector: string, text: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_INPUT, remainingMs(), force);
    await sleep(randRange(callCfg.field_switch_delay));
    await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    await sleep(rand(100, 250));
    const cdp = await ensureCdp();
    await humanType(page, rawKb, text, callCfg, cdp);
  };

  // --- fill (clears existing content first) ---
  const humanFillFn = async (selector: string, value: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_INPUT, remainingMs(), force);
    await sleep(randRange(callCfg.field_switch_delay));
    await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    await sleep(rand(100, 250));
    await originals.keyboardPress(SELECT_ALL);
    await sleep(rand(30, 80));
    await originals.keyboardPress('Backspace');
    await sleep(rand(50, 150));
    const cdp = await ensureCdp();
    await humanType(page, rawKb, value, callCfg, cdp);
  };

  // --- clear ---
  const humanClearFn = async (selector: string, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_FOCUS, remainingMs(), force);
    if (!await isSelectorFocused(stealth, page, selector)) {
      await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    }
    await sleep(rand(50, 150));
    await originals.keyboardPress(SELECT_ALL);
    await sleep(rand(30, 80));
    await originals.keyboardPress('Backspace');
  };

  // --- check ---
  const humanCheckFn = async (selector: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_CHECK, remainingMs(), force);
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }
    const checked = await originals.isChecked(selector).catch(() => false);
    if (!checked) {
      await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    }
  };

  // --- uncheck ---
  const humanUncheckFn = async (selector: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_CHECK, remainingMs(), force);
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }
    const checked = await originals.isChecked(selector).catch(() => true);
    if (checked) {
      await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    }
  };

  // --- selectOption ---
  const humanSelectOptionFn = async (selector: string, values: any, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_FOCUS, remainingMs(), force);
    await humanHoverFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    await sleep(rand(100, 300));
    return originals.selectOption(selector, values, options);
  };

  // --- press (checks focus first — avoids redundant mouse moves) ---
  const humanPressFn = async (selector: string, key: string, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_FOCUS, remainingMs(), force);
    if (!await isSelectorFocused(stealth, page, selector)) {
      await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    }
    await sleep(rand(50, 150));
    await originals.keyboardPress(key);
  };

  // --- pressSequentially ---
  const humanPressSequentiallyFn = async (selector: string, text: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    const timeout = options?.timeout ?? 30000;
    const force = options?.force ?? false;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());

    if (!force) await ensureActionable(page, selector, CHECKS_FOCUS, remainingMs(), force);
    if (!await isSelectorFocused(stealth, page, selector)) {
      await humanClickFn(selector, { _skipChecks: true, timeout: remainingMs(), force, human_config: options?.human_config } as any);
    }
    await sleep(rand(100, 250));
    const cdp = await ensureCdp();
    await humanType(page, rawKb, text, callCfg, cdp);
  };

  // --- tap ---
  const humanTapFn = async (selector: string, options?: HumanActionOptions) => {
    await humanClickFn(selector, options);
  };

  // Assign page-level patches
  (page as any).goto = humanGoto;
  (page as any).click = humanClickFn;
  (page as any).dblclick = humanDblclickFn;
  (page as any).hover = humanHoverFn;
  (page as any).type = humanTypeFn;
  (page as any).fill = humanFillFn;
  (page as any).check = humanCheckFn;
  (page as any).uncheck = humanUncheckFn;
  (page as any).selectOption = humanSelectOptionFn;
  (page as any).press = humanPressFn;
  (page as any).pressSequentially = humanPressSequentiallyFn;
  (page as any).tap = humanTapFn;
  (page as any).clear = humanClearFn;

  // --- mouse patches ---
  page.mouse.move = async (x: number, y: number, options?: {
    steps?: number;
  }) => {
    await ensureCursorInit();
    await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
    cursor.x = x;
    cursor.y = y;
  };

  page.mouse.click = async (x: number, y: number, options?: {
    button?: 'left' | 'right' | 'middle';
    clickCount?: number;
    delay?: number;
  }) => {
    await ensureCursorInit();
    await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
    cursor.x = x;
    cursor.y = y;
    await humanClick(raw, false, cfg);
  };

  // --- keyboard patches ---
  page.keyboard.type = async (text: string, options?: { delay?: number }) => {
    const cdp = await ensureCdp();
    await humanType(page, rawKb, text, cfg, cdp);
  };

  // Store helpers for frame patching
  (page as any)._humanCursor = cursor;
  (page as any)._humanRaw = raw;
  (page as any)._humanRawKb = rawKb;
  (page as any)._humanOriginals = originals;
  (page as any)._humanClickFn = humanClickFn;
  (page as any)._humanHoverFn = humanHoverFn;
  (page as any)._humanClearFn = humanClearFn;
  (page as any)._humanPressFn = humanPressFn;
  (page as any)._humanPressSequentiallyFn = humanPressSequentiallyFn;
  (page as any)._humanTapFn = humanTapFn;
  (page as any)._ensureCursorInit = ensureCursorInit;

  // Initialize cursor immediately so it doesn't visibly jump from (0,0)
  cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
  cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
  originals.mouseMove(cursor.x, cursor.y).then(() => {
    cursor.initialized = true;
  }).catch(() => {});

  // --- Patch Frame-level methods (for sub-frames) ---
  patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);

  // --- Patch ElementHandle selectors (page.$, page.$$, page.waitForSelector) ---
  patchPageElementHandles(page, cfg, cursor, raw, rawKb, originals, stealth);
}


// ============================================================================
// Frame-level patching
// ============================================================================

/**
 * Patch Frame methods so Locator-based calls go through humanization.
 * All 13 methods patched: click, dblclick, hover, type, fill, check, uncheck,
 * selectOption, press, pressSequentially, tap, clear, dragAndDrop.
 */
function patchFrames(
  page: Page,
  cfg: HumanConfig,
  cursor: CursorState,
  raw: RawMouse,
  rawKb: RawKeyboard,
  originals: any,
  stealth: StealthEval,
): void {
  for (const frame of iterFrames(page)) {
    patchSingleFrame(frame, page, cfg, cursor, raw, rawKb, originals, stealth);
    // Patch frame-level ElementHandle selectors ($, $$, waitForSelector)
    patchFrameElementHandles(frame, page, cfg, cursor, raw, rawKb, originals, stealth);
  }
}

function firstFrameLocator(frame: Frame, selector: string): any {
  const locator = frame.locator(selector) as any;
  return typeof locator.first === 'function' ? locator.first() : locator;
}

async function isFrameInputElement(frame: Frame, selector: string): Promise<boolean> {
  return firstFrameLocator(frame, selector).evaluate((el: Element) => {
    const tag = el.tagName.toLowerCase();
    return tag === 'input' || tag === 'textarea'
      || el.getAttribute('contenteditable') === 'true';
  }).catch(() => false);
}

async function isFrameSelectorFocused(frame: Frame, selector: string): Promise<boolean> {
  return firstFrameLocator(frame, selector).evaluate((el: Element) => el === document.activeElement)
    .catch(() => false);
}

function patchSingleFrame(
  frame: Frame,
  page: Page,
  cfg: HumanConfig,
  cursor: CursorState,
  raw: RawMouse,
  rawKb: RawKeyboard,
  originals: any,
  stealth: StealthEval,
): void {
  if ((frame as any)._humanPatched) return;
  (frame as any)._humanPatched = true;

  // Save originals for methods that need fallback
  const origFrameClick = frame.click.bind(frame);
  const origFrameDblclick = frame.dblclick.bind(frame);
  const origFrameHover = frame.hover.bind(frame);
  const origFrameType = frame.type.bind(frame);
  const origFrameFill = frame.fill.bind(frame);
  const origFrameCheck = frame.check.bind(frame);
  const origFrameUncheck = frame.uncheck.bind(frame);
  const origFrameSelectOption = frame.selectOption.bind(frame);
  const origFramePress = frame.press.bind(frame);
  const origFramePressSequentially = (frame as any).pressSequentially?.bind(frame);
  const origFrameTap = (frame as any).tap?.bind(frame);
  const origFrameDragAndDrop = frame.dragAndDrop.bind(frame);

  const moveToFrameSelector = async (
    selector: string,
    options: HumanActionOptions | undefined,
    inputBias: boolean,
    remainingMs: () => number,
  ) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    if (callCfg.idle_between_actions) {
      await humanIdle(raw, cursor.x, cursor.y, callCfg);
    }

    const locator = firstFrameLocator(frame, selector);
    if (typeof locator.scrollIntoViewIfNeeded === 'function') {
      await locator.scrollIntoViewIfNeeded({ timeout: Math.max(1, remainingMs()) }).catch(() => undefined);
    }
    const box = await locator.boundingBox({ timeout: Math.max(1, remainingMs()) }).catch(() => null);
    if (!box) return null;

    const isInput = inputBias || await isFrameInputElement(frame, selector);
    const target = clickTarget(box, isInput, callCfg);
    await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
    cursor.x = target.x;
    cursor.y = target.y;
    return { callCfg, isInput };
  };

  const frameClick = async (selector: string, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());
    const moved = await moveToFrameSelector(selector, options, false, remainingMs);
    if (!moved) return origFrameClick(selector, { ...options, timeout: Math.max(1, remainingMs()) });
    await humanClick(raw, moved.isInput, moved.callCfg);
  };

  const getFrameCdp = async () => stealth.getCdpSession().catch(() => null);

  const frameHover = async (selector: string, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());
    const moved = await moveToFrameSelector(selector, options, false, remainingMs);
    if (!moved) return origFrameHover(selector, { ...options, timeout: Math.max(1, remainingMs()) });
  };

  (frame as any).click = frameClick;

  (frame as any).dblclick = async (selector: string, options?: HumanActionOptions) => {
    const timeout = options?.timeout ?? 30000;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(0, deadline - Date.now());
    const moved = await moveToFrameSelector(selector, options, false, remainingMs);
    if (!moved) return origFrameDblclick(selector, { ...options, timeout: Math.max(1, remainingMs()) });
    await raw.down({ clickCount: 2 });
    await sleep(rand(30, 60));
    await raw.up({ clickCount: 2 });
  };

  (frame as any).hover = frameHover;

  (frame as any).type = async (selector: string, text: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    await sleep(randRange(callCfg.field_switch_delay));
    await frameClick(selector, options);
    await sleep(rand(100, 250));
    const cdp = await getFrameCdp();
    await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFrameType(selector, text, options));
  };

  (frame as any).fill = async (selector: string, value: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    await sleep(randRange(callCfg.field_switch_delay));
    await frameClick(selector, options);
    await sleep(rand(100, 250));
    await originals.keyboardPress(SELECT_ALL);
    await sleep(rand(30, 80));
    await originals.keyboardPress('Backspace');
    await sleep(rand(50, 150));
    const cdp = await getFrameCdp();
    await humanType(page, rawKb, value, callCfg, cdp).catch(() => origFrameFill(selector, value, options));
  };

  (frame as any).check = async (selector: string, options?: HumanActionOptions) => {
    const locator = firstFrameLocator(frame, selector);
    if (typeof locator.isChecked !== 'function') return origFrameCheck(selector, options);
    const checked = await locator.isChecked();
    if (!checked) await frameClick(selector, options).catch(() => origFrameCheck(selector, options));
  };

  (frame as any).uncheck = async (selector: string, options?: HumanActionOptions) => {
    const locator = firstFrameLocator(frame, selector);
    if (typeof locator.isChecked !== 'function') return origFrameUncheck(selector, options);
    const checked = await locator.isChecked();
    if (checked) await frameClick(selector, options).catch(() => origFrameUncheck(selector, options));
  };

  (frame as any).selectOption = async (selector: string, values: any, options?: HumanActionOptions) => {
    await frameHover(selector, options);
    await sleep(rand(100, 300));
    return origFrameSelectOption(selector, values, options);
  };

  (frame as any).press = async (selector: string, key: string, options?: HumanActionOptions) => {
    if (!await isFrameSelectorFocused(frame, selector)) {
      await frameClick(selector, options);
    }
    await sleep(rand(50, 150));
    await originals.keyboardPress(key);
  };

  (frame as any).pressSequentially = async (selector: string, text: string, options?: HumanActionOptions) => {
    const callCfg = mergeConfig(cfg, options?.human_config ?? options);
    if (!await isFrameSelectorFocused(frame, selector)) {
      await frameClick(selector, options);
    }
    await sleep(rand(100, 250));
    const cdp = await getFrameCdp();
    await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFramePressSequentially?.(selector, text, options));
  };

  (frame as any).tap = async (selector: string, options?: HumanActionOptions) => {
    await frameClick(selector, options).catch(() => origFrameTap?.(selector, options));
  };

  (frame as any).clear = async (selector: string, options?: HumanActionOptions) => {
    if (!await isFrameSelectorFocused(frame, selector)) {
      await frameClick(selector, options);
    }
    await sleep(rand(50, 150));
    await originals.keyboardPress(SELECT_ALL);
    await sleep(rand(30, 80));
    await originals.keyboardPress('Backspace');
  };

  (frame as any).dragAndDrop = async (source: string, target: string, options?: {
    force?: boolean;
    noWaitAfter?: boolean;
    sourcePosition?: { x: number; y: number };
    strict?: boolean;
    targetPosition?: { x: number; y: number };
    timeout?: number;
    trial?: boolean;
  }) => {
    const timeout = options?.timeout ?? 30000;
    const deadline = Date.now() + timeout;
    const remainingMs = () => Math.max(1, deadline - Date.now());
    const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: remainingMs() }).catch(() => null);
    const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: remainingMs() }).catch(() => null);

    if (srcBox && tgtBox) {
      const sx = srcBox.x + srcBox.width / 2;
      const sy = srcBox.y + srcBox.height / 2;
      const tx = tgtBox.x + tgtBox.width / 2;
      const ty = tgtBox.y + tgtBox.height / 2;

      await page.mouse.move(sx, sy);
      await sleep(rand(100, 200));
      await originals.mouseDown();
      await sleep(rand(80, 150));
      await page.mouse.move(tx, ty);
      await sleep(rand(80, 150));
      await originals.mouseUp();
    } else {
      return origFrameDragAndDrop(source, target, { ...options, timeout: Math.max(1, remainingMs()) });
    }
  };
}


function* iterFrames(page: Page): Generator<Frame> {
  try {
    const mainFrame = page.mainFrame();
    yield mainFrame;
    for (const child of mainFrame.childFrames()) {
      yield child;
    }
  } catch {}
}


// ============================================================================
// Context-level patching
// ============================================================================

function patchContext(context: BrowserContext, cfg: HumanConfig): void {
  const cursor = new CursorState();
  for (const page of context.pages()) {
    patchPage(page, cfg, cursor);
  }
  context.on('page', (page: Page) => {
    if (!(page as any)._original) {
      patchPage(page, cfg, new CursorState());
    }
  });

  const origNewPage = context.newPage.bind(context);
  (context as any).newPage = async () => {
    const page = await origNewPage();
    if (!(page as any)._original) {
      patchPage(page, cfg, new CursorState());
    }
    return page;
  };
}


// ============================================================================
// Browser-level patching
// ============================================================================

export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
  for (const context of browser.contexts()) {
    patchContext(context, cfg);
  }

  const origNewContext = browser.newContext.bind(browser);
  (browser as any).newContext = async (options?: Parameters<typeof origNewContext>[0]) => {
    const context = await origNewContext(options);
    patchContext(context, cfg);
    return context;
  };

  const origNewPage = browser.newPage.bind(browser);
  (browser as any).newPage = async (options?: Parameters<typeof origNewPage>[0]) => {
    const page = await origNewPage(options);
    if (!(page as any)._original) {
      const ctx = page.context();
      if (!(ctx as any)._humanPatched) {
        patchContext(ctx, cfg);
        (ctx as any)._humanPatched = true;
      }
      patchPage(page, cfg, new CursorState());
    }
    return page;
  };
}

export { patchContext, patchPage };
