#!/bin/zsh
# dom-introspect — extract framework-internal hooks (Vue 3 / React 16+ / Vue 2)
# from a DOM element in production builds where the obvious symbol names
# have been stripped or randomized. Surfaces props/handlers that JS-level
# automation can call directly.
#
# Usage:
#   dom-introspect --url-contains URL --selector SEL [--browser chrome|edge] [--xpath]
#                  [--show-keys] [--show-fiber] [--show-props]
#                  [--call HANDLER_NAME ARG_JSON]
#                  [--pretty]
#
# Output (one-line JSON unless --pretty):
#   {
#     "framework": "react"|"vue3"|"vue2"|"none",
#     "internal_keys": ["__reactProps$xxx", "__vnode", ...],
#     "fiber_chain": [...]   // (with --show-fiber)
#     "props": {...}         // (with --show-props, the props object)
#   }
#
# What it tries (in order):
#   1. Symbol-keyed properties (Vue 3 sometimes uses Symbols)
#   2. __vue_app__, __vnode, __vueParentComponent (Vue 3)
#   3. __vue__ (Vue 2)
#   4. __reactFiber$XXXX (React 16-17 dev)
#   5. __reactProps$XXXX (React 17+ — survives prod minification!)
#   6. __reactInternalInstance$XXXX (React 16 dev)
#   7. Walk up parents repeating the above (some frameworks attach to the
#      mount root, not every child)
set -euo pipefail
BIN_DIR=${0:A:h}

usage() {
  awk 'NR == 1 { next } /^set -/ { exit } { sub(/^# ?/, ""); print }' "${(%):-%x}"
}

url=""
selector=""
browser="Google Chrome"
xpath_mode=0
show_keys=0
show_fiber=0
show_props=0
call_handler=""
call_arg='null'
pretty=0

normalize_browser() {
  local raw="${1:-Google Chrome}"
  local key="${raw:l}"
  key="${key// /-}"
  case "$key" in
    chrome|google-chrome|googlechrome) echo "Google Chrome" ;;
    edge|microsoft-edge|microsoftedge|msedge) echo "Microsoft Edge" ;;
    *) echo "$raw" ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --url-contains) url="$2"; shift 2;;
    --selector)     selector="$2"; shift 2;;
    --browser)      browser="$(normalize_browser "$2")"; shift 2;;
    --xpath)        xpath_mode=1; shift;;
    --show-keys)    show_keys=1; shift;;
    --show-fiber)   show_fiber=1; shift;;
    --show-props)   show_props=1; shift;;
    --call)         call_handler="$2"; call_arg="$3"; shift 3;;
    --pretty)       pretty=1; shift;;
    -h|--help)      usage; exit 0;;
    *) echo "dom-introspect: unknown arg: $1" >&2; exit 2;;
  esac
done

if [[ -z "$url" || -z "$selector" ]]; then
  echo 'dom-introspect: --url-contains and --selector are required' >&2
  exit 2
fi

SEL_JSON=$(SEL="$selector" /usr/bin/python3 -c 'import json,os;print(json.dumps(os.environ["SEL"]))')
CALL_JSON=$(CALL="$call_handler" /usr/bin/python3 -c 'import json,os;print(json.dumps(os.environ["CALL"]))')

JS_FILE=$(/usr/bin/mktemp -t domintrospect.XXXXXX.js)
trap "rm -f $JS_FILE" EXIT

/bin/cat > "$JS_FILE" <<JS
(() => {
  const sel = ${SEL_JSON};
  const isXPath = ${xpath_mode} === 1 || sel.startsWith("/") || sel.startsWith("(/");
  let el = null;
  if (isXPath) {
    el = document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  } else {
    el = document.querySelector(sel);
  }
  if (!el) return {error: "selector matched nothing"};

  function reactInternals(node) {
    var keys = Object.keys(node);
    var fiberKey = keys.find(function(k){return k.startsWith("__reactFiber");});
    var propsKey = keys.find(function(k){return k.startsWith("__reactProps");});
    var instKey  = keys.find(function(k){return k.startsWith("__reactInternalInstance");});
    return {fiberKey: fiberKey, propsKey: propsKey, instKey: instKey};
  }
  function vue3Internals(node) {
    return {
      vue_app: !!node.__vue_app__,
      vnode: !!node.__vnode,
      parent_component: !!node.__vueParentComponent,
    };
  }
  function vue2Internals(node) {
    return {vue_instance: !!node.__vue__};
  }

  function probe(node) {
    var keys = Object.keys(node);
    var symbols = Object.getOwnPropertySymbols(node).map(function(s){return s.toString();});
    var dunder = keys.filter(function(k){return k.startsWith("__");});
    return {
      tag: node.tagName,
      keys: dunder,
      symbols: symbols,
      react: reactInternals(node),
      vue3: vue3Internals(node),
      vue2: vue2Internals(node),
    };
  }

  function frameworkOf(probeResult) {
    if (probeResult.react.fiberKey || probeResult.react.propsKey) return "react";
    if (probeResult.vue3.vue_app || probeResult.vue3.vnode || probeResult.vue3.parent_component) return "vue3";
    if (probeResult.vue2.vue_instance) return "vue2";
    return "none";
  }

  // Walk: try the element itself first; if "none", walk up to mount root
  var current = el;
  var probes = [];
  for (var depth = 0; depth < 10 && current; depth++) {
    var p = probe(current);
    probes.push(p);
    if (frameworkOf(p) !== "none") { current = current; break; }
    current = current.parentElement;
  }
  var hit = probes[probes.length - 1];
  var framework = frameworkOf(hit);

  var out = {framework: framework, depth: probes.length - 1, tag_chain: probes.map(function(p){return p.tag;})};

  if (${show_keys}) {
    out.internal_keys = hit.keys;
    out.symbols = hit.symbols;
  }

  if (${show_fiber} || ${show_props}) {
    var node = current || el;
    var keys = Object.keys(node);
    var pk = keys.find(function(k){return k.startsWith("__reactProps");});
    if (pk) {
      var props = node[pk];
      if (${show_props}) {
        var safeProps = {};
        if (props) {
          for (var k in props) {
            try {
              var v = props[k];
              safeProps[k] = (typeof v === "function") ? "[function]" : (typeof v === "object" ? "[object]" : v);
            } catch (e) { safeProps[k] = "[unreadable: " + e.message + "]"; }
          }
        }
        out.props = safeProps;
      }
    }
    var fk = keys.find(function(k){return k.startsWith("__reactFiber");});
    if (fk && ${show_fiber}) {
      var fiber = node[fk];
      var chain = [];
      var f = fiber;
      while (f && chain.length < 10) {
        var memProps = (f.memoizedProps && typeof f.memoizedProps === "object") ? Object.keys(f.memoizedProps) : null;
        chain.push({type: typeof f.type === "string" ? f.type : (f.type && (f.type.name || f.type.displayName)) || "?", memoizedProps_keys: memProps});
        f = f.return;
      }
      out.fiber_chain = chain;
    }
  }

  // Optionally call a handler from props
  var callName = ${CALL_JSON};
  if (callName) {
    var node = current || el;
    var keys = Object.keys(node);
    var pk = keys.find(function(k){return k.startsWith("__reactProps");});
    if (!pk) return {error: "no __reactProps* on node — callable handlers only available for React"};
    var props = node[pk];
    var fn = props[callName];
    if (typeof fn !== "function") return {error: "handler not found or not a function", available: Object.keys(props || {})};
    try {
      fn(${call_arg});
      out.called = callName;
    } catch (e) {
      out.call_error = e.message;
    }
  }

  return out;
})()
JS

RAW=$("$BIN_DIR/background-web-control" --browser "$browser" eval --url-contains "$url" --js-file "$JS_FILE" 2>&1 || true)

PARSED=$(echo "$RAW" | /usr/bin/python3 -c '
import sys, json
text = sys.stdin.read()
i = text.find("\"value\":")
if i < 0:
    snippet = text[:300].replace("\n", " ").replace("\r", " ")
    print(json.dumps({"_no_value": True, "_raw": snippet})); sys.exit(0)
j = i + 8
while j < len(text) and text[j] in " \t\n\r":
    j += 1
opener = text[j] if j < len(text) else None
if opener != "{":
    print(json.dumps({"_no_obj": True, "_raw_at_j": text[j:j+100]})); sys.exit(0)
depth, in_str, esc, end = 0, False, False, -1
for k in range(j, len(text)):
    ch = text[k]
    if in_str:
        if esc: esc = False
        elif ch == "\\": esc = True
        elif ch == "\"": in_str = False
    else:
        if ch == "\"": in_str = True
        elif ch == "{": depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0: end = k+1; break
if end < 0:
    print(json.dumps({"_unbalanced": True})); sys.exit(0)
try:
    obj = json.loads(text[j:end], strict=False)
    print(json.dumps(obj, ensure_ascii=False))
except Exception as exc:
    print(json.dumps({"_parse_err": str(exc), "_raw": text[j:end][:300]}))
')

if [[ $pretty -eq 1 ]]; then
  echo "$PARSED" | /usr/bin/python3 -m json.tool
else
  echo "$PARSED"
fi
