"""React-safe input helpers — UC-mode safe (no `arguments[]`).

SeleniumBase UC mode's `execute_script` runs under CDP's Runtime.evaluate
which doesn't bind `arguments` the way vanilla Selenium does. Every helper
here bakes values into the JS source via `json.dumps(...)` so we never
depend on `arguments[0]`.
"""
import json
import time
from typing import Any, Optional


def _js(script: str, subs: dict) -> str:
    """Substitute `__NAME__` placeholders with JSON-encoded values."""
    out = script
    for k, v in subs.items():
        out = out.replace(f"__{k}__", json.dumps(v))
    return out


_NATIVE_SET_TMPL = r"""
(function() {
  var sel = __SEL__;
  var value = __VALUE__;
  function find(s) {
    var nodes = document.querySelectorAll(s);
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      var r = n.getBoundingClientRect();
      var st = window.getComputedStyle(n);
      if (st.display === 'none' || st.visibility === 'hidden' || parseFloat(st.opacity) === 0) continue;
      if (r.width === 0 && r.height === 0) continue;
      return n;
    }
    return null;
  }
  var el = find(sel);
  if (!el) return { ok: false, error: 'not found: ' + sel };
  var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype
    : el.tagName === 'SELECT' ? HTMLSelectElement.prototype
    : HTMLInputElement.prototype;
  var setter = Object.getOwnPropertyDescriptor(proto, 'value');
  try { el.focus(); } catch (e) {}
  if (setter && setter.set) setter.set.call(el, String(value == null ? '' : value));
  else el.value = String(value == null ? '' : value);
  el.dispatchEvent(new Event('input',  { bubbles: true }));
  el.dispatchEvent(new Event('change', { bubbles: true }));
  el.dispatchEvent(new Event('blur',   { bubbles: true }));
  return { ok: true };
})();
"""


def fill_instant(driver, selector: str, value: Any) -> bool:
    js = _js(_NATIVE_SET_TMPL, {"SEL": selector, "VALUE": "" if value is None else str(value)})
    try:
        res = driver.execute_script(js)
    except Exception:
        return False
    return bool(res and res.get("ok"))


_TYPE_INTO_TMPL = r"""
(function() {
  var sel = __SEL__;
  var value = __VALUE__;
  function find(s) {
    var nodes = document.querySelectorAll(s);
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      var r = n.getBoundingClientRect();
      var st = window.getComputedStyle(n);
      if (st.display === 'none' || st.visibility === 'hidden' || parseFloat(st.opacity) === 0) continue;
      if (r.width === 0 && r.height === 0) continue;
      return n;
    }
    return null;
  }
  var el = find(sel);
  if (!el) return { ok: false, error: 'not found: ' + sel };
  var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
  var setter = Object.getOwnPropertyDescriptor(proto, 'value');
  try { el.focus(); } catch (e) {}
  if (setter && setter.set) setter.set.call(el, '');
  else el.value = '';
  el.dispatchEvent(new Event('input', { bubbles: true }));
  var chars = value.split('');
  for (var i = 0; i < chars.length; i++) {
    var next = el.value + chars[i];
    if (setter && setter.set) setter.set.call(el, next);
    else el.value = next;
    el.dispatchEvent(new InputEvent('input', { bubbles: true, data: chars[i], inputType: 'insertText' }));
  }
  el.dispatchEvent(new Event('change', { bubbles: true }));
  el.dispatchEvent(new Event('blur', { bubbles: true }));
  return { ok: true };
})();
"""


def type_into(driver, selector: str, value: str, per_char_delay_ms: int = 30) -> bool:
    # per_char_delay_ms kept for API compat — CDP execute_script doesn't
    # honour setTimeout inside its synchronous return, so we fire InputEvents
    # back-to-back. Every char still produces an InputEvent so React
    # validators observe the typing sequence.
    del per_char_delay_ms
    js = _js(_TYPE_INTO_TMPL, {"SEL": selector, "VALUE": str(value)})
    try:
        res = driver.execute_script(js)
    except Exception:
        return fill_instant(driver, selector, value)
    if res and res.get("ok"):
        return True
    return fill_instant(driver, selector, value)


_CLICK_BY_TEXT_TMPL = r"""
(function() {
  var text = __TEXT__;
  var roles = __ROLES__;
  var lower = String(text).toLowerCase().trim();
  function visible(n) {
    if (!n) return false;
    var r = n.getBoundingClientRect();
    var s = window.getComputedStyle(n);
    if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;
    return !(r.width === 0 && r.height === 0);
  }
  for (var ri = 0; ri < roles.length; ri++) {
    var nodes = document.querySelectorAll(roles[ri]);
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      if (!visible(n)) continue;
      var t = (n.innerText || n.value || n.getAttribute('aria-label') || '').trim().toLowerCase();
      if (t === lower) { n.click(); return true; }
    }
  }
  for (var ri2 = 0; ri2 < roles.length; ri2++) {
    var nodes2 = document.querySelectorAll(roles[ri2]);
    for (var j = 0; j < nodes2.length; j++) {
      var n2 = nodes2[j];
      if (!visible(n2)) continue;
      var t2 = (n2.innerText || n2.value || n2.getAttribute('aria-label') || '').trim().toLowerCase();
      if (t2.indexOf(lower) !== -1) { n2.click(); return true; }
    }
  }
  return false;
})();
"""


def click_by_text(driver, text: str, roles=None) -> bool:
    if roles is None:
        roles = ["button", "a", '[role="button"]']
    js = _js(_CLICK_BY_TEXT_TMPL, {"TEXT": text, "ROLES": list(roles)})
    try:
        return bool(driver.execute_script(js))
    except Exception:
        return False


_WAIT_FIND_TMPL = r"""
(function() {
  var sels = __SELS__;
  for (var si = 0; si < sels.length; si++) {
    var s = sels[si];
    var nodes = document.querySelectorAll(s);
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      var r = n.getBoundingClientRect();
      var st = window.getComputedStyle(n);
      if (st.display === 'none' || st.visibility === 'hidden' || parseFloat(st.opacity) === 0) continue;
      if (r.width === 0 && r.height === 0) continue;
      return s;
    }
  }
  return null;
})();
"""


def wait_for_selector(driver, selectors, timeout_s: float = 10.0, poll_s: float = 0.15) -> Optional[str]:
    if isinstance(selectors, str):
        selectors = [selectors]
    js = _js(_WAIT_FIND_TMPL, {"SELS": list(selectors)})
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        try:
            hit = driver.execute_script(js)
            if hit:
                return hit
        except Exception:
            pass
        time.sleep(poll_s)
    return None


_IS_VISIBLE_TMPL = r"""
(function() {
  var s = __SEL__;
  var n = document.querySelector(s);
  if (!n) return false;
  var r = n.getBoundingClientRect();
  var st = window.getComputedStyle(n);
  if (st.display === 'none' || st.visibility === 'hidden' || parseFloat(st.opacity) === 0) return false;
  return !(r.width === 0 && r.height === 0);
})();
"""


def is_visible(driver, selector: str) -> bool:
    js = _js(_IS_VISIBLE_TMPL, {"SEL": selector})
    try:
        return bool(driver.execute_script(js))
    except Exception:
        return False


def set_select(driver, selector: str, value: Any) -> bool:
    return fill_instant(driver, selector, value)


_CHECK_RADIO_TMPL = r"""
(function() {
  var wants = __WANTS__;
  var lowerWants = wants.map(function(w) { return String(w).toLowerCase(); });
  var radios = document.querySelectorAll('input[type="radio"]');
  for (var i = 0; i < radios.length; i++) {
    var r = radios[i];
    var val = (r.value || '').toLowerCase();
    var id = (r.id || '').toLowerCase();
    var match = lowerWants.some(function(w) { return val === w || id === w || val.indexOf(w) !== -1 || id.indexOf(w) !== -1; });
    if (match) {
      var label = r.id ? document.querySelector('label[for="' + CSS.escape(r.id) + '"]') : null;
      (label || r).click();
      return true;
    }
  }
  return false;
})();
"""


def check_radio_by_value(driver, values) -> bool:
    if isinstance(values, str):
        values = [values]
    js = _js(_CHECK_RADIO_TMPL, {"WANTS": list(values)})
    try:
        return bool(driver.execute_script(js))
    except Exception:
        return False


_SHOW_PROMPT_TMPL = r"""
(function() {
  try {
    var text = __TEXT__;
    var old = document.getElementById('__ffsb_prompt');
    if (old) old.remove();
    var b = document.createElement('div');
    b.id = '__ffsb_prompt';
    b.textContent = text;
    var s = b.style;
    s.position = 'fixed'; s.top = '12px'; s.left = '50%';
    s.transform = 'translateX(-50%)';
    s.zIndex = 2147483647; s.background = '#b58aff'; s.color = '#1a0d2a';
    s.padding = '12px 20px'; s.borderRadius = '12px'; s.fontSize = '14px';
    s.fontWeight = '700'; s.fontFamily = 'ui-sans-serif, system-ui, sans-serif';
    s.boxShadow = '0 8px 24px rgba(0,0,0,0.6)'; s.maxWidth = '460px';
    s.textAlign = 'center'; s.pointerEvents = 'none';
    document.documentElement.appendChild(b);
  } catch (e) {}
})();
"""


def show_prompt(driver, text: str) -> None:
    js = _js(_SHOW_PROMPT_TMPL, {"TEXT": text})
    try:
        driver.execute_script(js)
    except Exception:
        pass


def hide_prompt(driver) -> None:
    js = "try { var b = document.getElementById('__ffsb_prompt'); if (b) b.remove(); } catch(e) {}"
    try:
        driver.execute_script(js)
    except Exception:
        pass
