// retoor <retoor@molodetz.nl>

const MAX_RESULT_CHARS = 8000;
const NAV_DELAY_MS = 150;
const LABEL_MAX = 120;
const VALUE_MAX = 200;
const DISCOVER_LIMIT = 40;
const SETTLE_MS = 350;
const WAIT_POLL_MS = 150;
const WAIT_CAP_MS = 55000;

const DISCOVER_SELECTOR = [
  "a[href]",
  "button",
  "[role='button']",
  "[role='link']",
  "[role='tab']",
  "[role='menuitem']",
  "input:not([type='hidden'])",
  "textarea",
  "select",
  "[contenteditable='']",
  "[contenteditable='true']",
  "summary",
  ".poll-option",
  "[data-modal]",
  "[data-devii-open]",
].join(", ");

export default class DeviiClient {
  constructor() {
    this._highlights = [];
    this._reposition = () => this._repositionHighlights();
    this._refs = new Map();
  }

  async execute(action, args = {}) {
    try {
      return await this._dispatch(action, args);
    } catch (error) {
      return { error: String(error && error.message ? error.message : error) };
    }
  }

  async _dispatch(action, args = {}) {
    switch (action) {
      case "get_page_context":
        return this._context();
      case "run_js":
        return await this._runJs(String(args.code || ""));
      case "discover_elements":
        return this._discover(args);
      case "read_element":
        return this._readElement(String(args.target || ""));
      case "click_element":
        return await this._click(String(args.target || ""), String(args.button || "left"));
      case "fill_field":
        return await this._fill(String(args.target || ""), String(args.value ?? ""));
      case "set_control":
        return this._setControl(args);
      case "submit_form":
        return await this._submitForm(args);
      case "wait_for":
        return await this._waitFor(args);
      case "press_key":
        return this._pressKey(args);
      case "run_sequence":
        return await this._runSequence(args.steps || []);
      case "highlight_element":
        return this._highlight(String(args.selector || args.target || ""), args.label);
      case "clear_highlights":
        return this._clearHighlights();
      case "show_toast":
        return this._toast(String(args.text || ""), Number(args.duration_ms) || 4000);
      case "scroll_to_element":
        return this._scrollTo(String(args.selector || args.target || ""));
      case "navigate_to":
        return this._navigate(String(args.url || ""));
      case "reload_page":
        return this._reload();
      case "open_terminal":
        return this._openTerminal(args);
      default:
        return { error: `unknown client action '${action}'` };
    }
  }

  _context() {
    const headings = Array.from(document.querySelectorAll("h1, h2, h3"))
      .map((h) => h.textContent.trim())
      .filter(Boolean)
      .slice(0, 25);
    const selection = String(window.getSelection ? window.getSelection().toString() : "").slice(0, 2000);
    const docs = window.DEVPLACE_DOCS || {};
    let loggedIn = !!docs.loggedIn;
    let username = docs.username || "";
    if (!username) {
      const nameEl = document.querySelector(".topnav-user-name");
      if (nameEl) {
        username = nameEl.textContent.trim();
        loggedIn = true;
      }
    }
    const pageTypeEl = document.querySelector("[data-page-type]");
    const pageType = pageTypeEl ? pageTypeEl.getAttribute("data-page-type") : location.pathname;
    const snap = this._snapshot();
    const interactiveCount = document.querySelectorAll(DISCOVER_SELECTOR).length;
    return {
      url: location.href,
      path: location.pathname + location.search,
      pageType,
      title: document.title,
      referrer: document.referrer || null,
      viewport: { width: window.innerWidth, height: window.innerHeight },
      scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY) },
      selection,
      headings,
      loggedIn,
      username: username || null,
      openModal: snap.modal,
      toasts: snap.toasts,
      activeElement: this._briefLabel(document.activeElement),
      interactiveCount,
    };
  }

  async _runJs(code) {
    if (!code.trim()) return { error: "no code provided" };
    const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
    const fn = new AsyncFunction(code);
    const value = await fn();
    return this._serialize(value);
  }

  _serialize(value) {
    if (value === undefined) return null;
    let out;
    try {
      out = JSON.parse(JSON.stringify(value));
    } catch (error) {
      out = String(value);
    }
    const text = typeof out === "string" ? out : JSON.stringify(out);
    if (text && text.length > MAX_RESULT_CHARS) {
      const base = typeof out === "string" ? out : text;
      return base.slice(0, MAX_RESULT_CHARS) + " …[truncated]";
    }
    return out;
  }

  _visible(element) {
    if (!element || !element.getClientRects) return false;
    if (element.getClientRects().length === 0) return false;
    const style = getComputedStyle(element);
    return style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity) !== 0;
  }

  _enabled(element) {
    return !element.disabled && element.getAttribute("aria-disabled") !== "true";
  }

  _role(element) {
    const explicit = element.getAttribute("role");
    if (explicit) return explicit;
    const tag = element.tagName.toLowerCase();
    if (tag === "a") return "link";
    if (tag === "button" || tag === "summary") return "button";
    if (tag === "textarea") return "textbox";
    if (tag === "select") return "select";
    if (tag === "input") {
      const type = (element.getAttribute("type") || "text").toLowerCase();
      if (type === "checkbox") return "checkbox";
      if (type === "radio") return "radio";
      if (type === "submit" || type === "button") return "button";
      return "input";
    }
    if (element.isContentEditable) return "textbox";
    return "element";
  }

  _label(element) {
    const aria = element.getAttribute("aria-label");
    if (aria) return aria.trim().slice(0, LABEL_MAX);
    const labelledby = element.getAttribute("aria-labelledby");
    if (labelledby) {
      const text = labelledby
        .split(/\s+/)
        .map((id) => {
          const el = document.getElementById(id);
          return el ? el.textContent.trim() : "";
        })
        .filter(Boolean)
        .join(" ");
      if (text) return text.slice(0, LABEL_MAX);
    }
    const tag = element.tagName.toLowerCase();
    if (tag === "input" || tag === "textarea" || tag === "select") {
      const id = element.getAttribute("id");
      if (id) {
        const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
        if (lab && lab.textContent.trim()) return lab.textContent.trim().slice(0, LABEL_MAX);
      }
      const wrap = element.closest("label");
      if (wrap && wrap.textContent.trim()) return wrap.textContent.trim().slice(0, LABEL_MAX);
      const placeholder = element.getAttribute("placeholder");
      if (placeholder) return placeholder.trim().slice(0, LABEL_MAX);
      if (tag !== "textarea" && element.value) return String(element.value).slice(0, LABEL_MAX);
    }
    const text = (element.textContent || "").replace(/\s+/g, " ").trim();
    if (text) return text.slice(0, LABEL_MAX);
    const title = element.getAttribute("title");
    if (title) return title.trim().slice(0, LABEL_MAX);
    const img = element.querySelector("img[alt]");
    if (img) return img.getAttribute("alt").trim().slice(0, LABEL_MAX);
    return "";
  }

  _briefLabel(element) {
    if (!element || element === document.body) return null;
    const role = this._role(element);
    const label = this._label(element);
    return label ? `${role}: ${label}`.slice(0, LABEL_MAX) : role;
  }

  _state(element) {
    const tag = element.tagName.toLowerCase();
    const state = {
      visible: this._visible(element),
      enabled: this._enabled(element),
      inViewport: false,
    };
    const rect = element.getBoundingClientRect();
    state.inViewport =
      rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
    if (tag === "input") {
      const type = (element.getAttribute("type") || "text").toLowerCase();
      if (type === "checkbox" || type === "radio") state.checked = !!element.checked;
      else state.value = String(element.value || "").slice(0, VALUE_MAX);
    } else if (tag === "textarea" || tag === "select") {
      state.value = String(element.value || "").slice(0, VALUE_MAX);
    }
    const expanded = element.getAttribute("aria-expanded");
    if (expanded !== null) state.expanded = expanded === "true";
    return state;
  }

  _rect(element) {
    const r = element.getBoundingClientRect();
    return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
  }

  _cssPath(element) {
    const id = element.getAttribute("id");
    if (id && document.querySelectorAll(`#${CSS.escape(id)}`).length === 1) {
      return `#${CSS.escape(id)}`;
    }
    const parts = [];
    let node = element;
    let depth = 0;
    while (node && node.nodeType === 1 && depth < 4) {
      let segment = node.tagName.toLowerCase();
      const nid = node.getAttribute("id");
      if (nid && document.querySelectorAll(`#${CSS.escape(nid)}`).length === 1) {
        parts.unshift(`#${CSS.escape(nid)}`);
        break;
      }
      const parent = node.parentElement;
      if (parent) {
        const sameTag = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
        if (sameTag.length > 1) segment += `:nth-of-type(${sameTag.indexOf(node) + 1})`;
      }
      parts.unshift(segment);
      node = node.parentElement;
      depth += 1;
    }
    return parts.join(" > ").slice(0, LABEL_MAX);
  }

  _container(element) {
    const modal = element.closest(".modal-overlay, dialog, dp-dialog");
    if (modal) return modal.getAttribute("id") ? `#${modal.getAttribute("id")}` : modal.tagName.toLowerCase();
    const form = element.closest("form");
    if (form) return form.getAttribute("id") ? `#${form.getAttribute("id")}` : "form";
    return null;
  }

  _matchesKind(role, kind) {
    if (!kind) return true;
    const k = kind.toLowerCase();
    if (k === "input" || k === "textbox") return role === "input" || role === "textbox";
    return role === k;
  }

  _describe(element, ref) {
    return {
      ref,
      role: this._role(element),
      label: this._label(element),
      tag: element.tagName.toLowerCase(),
      selector: this._cssPath(element),
      state: this._state(element),
      rect: this._rect(element),
      container: this._container(element),
    };
  }

  _discover(args = {}) {
    const query = String(args.query || "").trim().toLowerCase();
    const kind = String(args.kind || "").trim();
    const limit = Math.max(1, Math.min(Number(args.limit) || DISCOVER_LIMIT, 100));
    let root = document;
    if (args.within) {
      try {
        root = this._resolveTarget(String(args.within));
      } catch (error) {
        return { error: `within: ${error.message}` };
      }
    }
    const seen = new Set();
    const elements = [];
    this._refs = new Map();
    let index = 0;
    const candidates = root.querySelectorAll(DISCOVER_SELECTOR);
    for (const element of candidates) {
      if (seen.has(element)) continue;
      seen.add(element);
      if (!this._visible(element)) continue;
      const role = this._role(element);
      if (!this._matchesKind(role, kind)) continue;
      const record = this._describe(element, `e${index + 1}`);
      if (query && !record.label.toLowerCase().includes(query)) continue;
      index += 1;
      record.ref = `e${index}`;
      this._refs.set(record.ref, element);
      elements.push(record);
      if (elements.length >= limit) break;
    }
    return {
      count: elements.length,
      truncated: elements.length >= limit,
      scope: args.within ? String(args.within) : "page",
      openModal: this._snapshot().modal,
      elements,
    };
  }

  _readElement(target) {
    const element = this._resolveTarget(target);
    const attrs = {};
    for (const name of ["id", "name", "type", "href", "value", "placeholder", "class"]) {
      const value = element.getAttribute(name);
      if (value) attrs[name] = String(value).slice(0, VALUE_MAX);
    }
    return {
      tag: element.tagName.toLowerCase(),
      role: this._role(element),
      label: this._label(element),
      text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 600),
      selector: this._cssPath(element),
      state: this._state(element),
      rect: this._rect(element),
      container: this._container(element),
      attributes: attrs,
      error: this._nearbyError(element),
    };
  }

  _resolveTarget(target) {
    const value = String(target || "").trim();
    if (!value) throw new Error("no target provided");
    if (/^e\d+$/.test(value)) {
      const element = this._refs.get(value);
      if (element && element.isConnected) return element;
    }
    let element = null;
    try {
      element = document.querySelector(value);
    } catch (error) {
      element = null;
    }
    if (!element) element = this._findByText(value);
    if (!element) throw new Error(`no element matches '${value}'`);
    return element;
  }

  _resolve(selector) {
    return this._resolveTarget(selector);
  }

  _findByText(text) {
    const needle = String(text).trim();
    if (!needle) return null;
    const candidates = document.querySelectorAll(
      "h1, h2, h3, h4, h5, h6, a, button, [role='button'], label, summary, li, th, td, p, span",
    );
    let partial = null;
    for (const el of candidates) {
      if (el.offsetParent === null && getComputedStyle(el).position !== "fixed") continue;
      const content = (el.textContent || "").trim();
      if (!content) continue;
      if (content === needle) return el;
      if (!partial && content.includes(needle) && content.length <= needle.length + 60) {
        partial = el;
      }
    }
    return partial;
  }

  _snapshot() {
    const overlays = Array.from(
      document.querySelectorAll(".modal-overlay.visible, .modal-overlay.show, dialog[open], dp-dialog[open]"),
    ).filter((el) => this._visible(el));
    const top = overlays[overlays.length - 1];
    let modal = null;
    if (top) modal = top.getAttribute("id") ? `#${top.getAttribute("id")}` : top.tagName.toLowerCase();
    const toasts = Array.from(document.querySelectorAll(".dp-toast, .toast, .devii-toast, [class*='toast']"))
      .filter((el) => this._visible(el))
      .map((el) => el.textContent.trim())
      .filter(Boolean)
      .slice(0, 5);
    return { url: location.pathname + location.search, modal, toasts };
  }

  _nearbyError(element) {
    const scope = element.closest("form, .modal-overlay, .field, .form-group") || document;
    const node = scope.querySelector(
      ".field-error, .form-error, .error-message, .input-error, [aria-invalid='true']",
    );
    if (node && this._visible(node)) {
      const text = (node.textContent || "").trim();
      if (text) return text.slice(0, VALUE_MAX);
    }
    return null;
  }

  _delta(before, after, element) {
    const delta = {};
    if (before.url !== after.url) {
      delta.urlChanged = true;
      delta.navigatedTo = after.url;
    }
    if (before.modal !== after.modal) {
      if (after.modal && !before.modal) delta.modalOpened = after.modal;
      else if (!after.modal && before.modal) delta.modalClosed = before.modal;
      else delta.modalChanged = after.modal;
    }
    const fresh = after.toasts.filter((t) => !before.toasts.includes(t));
    if (fresh.length) delta.toast = fresh.join(" | ").slice(0, VALUE_MAX);
    if (element && !element.isConnected) delta.elementGone = true;
    const error = element && element.isConnected ? this._nearbyError(element) : null;
    if (error) delta.error = error;
    return delta;
  }

  _sleep(ms) {
    return new Promise((resolve) => window.setTimeout(resolve, ms));
  }

  async _waitVisibleEnabled(element, timeout) {
    const deadline = Date.now() + timeout;
    while (Date.now() < deadline) {
      if (this._visible(element) && this._enabled(element)) return true;
      await this._sleep(WAIT_POLL_MS);
    }
    return this._visible(element) && this._enabled(element);
  }

  _fireMouse(element, type) {
    const rect = element.getBoundingClientRect();
    const opts = {
      bubbles: true,
      cancelable: true,
      view: window,
      clientX: Math.round(rect.left + rect.width / 2),
      clientY: Math.round(rect.top + rect.height / 2),
    };
    element.dispatchEvent(new MouseEvent(type, opts));
  }

  _fireClick(element, button) {
    element.focus && element.focus();
    if (button === "right") {
      this._fireMouse(element, "contextmenu");
      return;
    }
    this._fireMouse(element, "pointerdown");
    this._fireMouse(element, "mousedown");
    this._fireMouse(element, "pointerup");
    this._fireMouse(element, "mouseup");
    this._fireMouse(element, "click");
    if (button === "double") {
      this._fireMouse(element, "click");
      this._fireMouse(element, "dblclick");
    }
  }

  async _click(target, button) {
    const element = this._resolveTarget(target);
    const label = this._briefLabel(element);
    element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
    const ready = await this._waitVisibleEnabled(element, 3000);
    if (!ready) {
      return { error: `element not clickable`, target, state: this._state(element) };
    }
    const before = this._snapshot();
    this._fireClick(element, button);
    await this._sleep(SETTLE_MS);
    const after = this._snapshot();
    return { clicked: label, button, ...this._delta(before, after, element) };
  }

  _setNativeValue(element, value) {
    const proto =
      element instanceof window.HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto, "value");
    if (setter && setter.set) setter.set.call(element, value);
    else element.value = value;
  }

  _codeMirrorOf(element) {
    let host = element.closest && element.closest(".CodeMirror");
    if (host && host.CodeMirror) return host.CodeMirror;
    const parent = element.parentElement;
    if (parent) {
      host = parent.querySelector(".CodeMirror");
      if (host && host.CodeMirror) return host.CodeMirror;
    }
    return null;
  }

  async _fill(target, value) {
    const element = this._resolveTarget(target);
    const label = this._briefLabel(element);
    element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
    const cm = this._codeMirrorOf(element);
    if (cm) {
      cm.setValue(value);
      if (cm.save) cm.save();
      return { filled: label, via: "codemirror", value: value.slice(0, VALUE_MAX) };
    }
    if (element.isContentEditable) {
      element.focus && element.focus();
      element.textContent = value;
      element.dispatchEvent(new Event("input", { bubbles: true }));
      return { filled: label, via: "contenteditable", value: value.slice(0, VALUE_MAX) };
    }
    const tag = element.tagName.toLowerCase();
    if (tag !== "input" && tag !== "textarea" && tag !== "select") {
      return { error: `'${target}' is a ${tag}, not a fillable field` };
    }
    element.focus && element.focus();
    if (tag === "select") {
      element.value = value;
    } else {
      this._setNativeValue(element, value);
    }
    element.dispatchEvent(new Event("input", { bubbles: true }));
    element.dispatchEvent(new Event("change", { bubbles: true }));
    return { filled: label, value: String(element.value).slice(0, VALUE_MAX), error: this._nearbyError(element) };
  }

  _setControl(args = {}) {
    const element = this._resolveTarget(String(args.target || ""));
    const label = this._briefLabel(element);
    const tag = element.tagName.toLowerCase();
    if (tag === "select") {
      const wanted = String(args.option ?? "");
      let chosen = null;
      for (const opt of element.options) {
        if (opt.value === wanted || opt.textContent.trim() === wanted) {
          chosen = opt.value;
          break;
        }
      }
      if (chosen === null) {
        for (const opt of element.options) {
          if (opt.textContent.trim().toLowerCase().includes(wanted.toLowerCase())) {
            chosen = opt.value;
            break;
          }
        }
      }
      if (chosen === null) return { error: `no option matches '${wanted}'` };
      element.value = chosen;
      element.dispatchEvent(new Event("change", { bubbles: true }));
      return { set: label, value: chosen };
    }
    const type = (element.getAttribute("type") || "").toLowerCase();
    if (type !== "checkbox" && type !== "radio") {
      return { error: `'${args.target}' is not a checkbox, radio, or select` };
    }
    const want = args.checked === undefined ? !element.checked : !!args.checked;
    element.checked = type === "radio" ? true : want;
    element.dispatchEvent(new Event("input", { bubbles: true }));
    element.dispatchEvent(new Event("change", { bubbles: true }));
    return { set: label, checked: element.checked };
  }

  _fieldInForm(form, key) {
    let field = form.querySelector(`[name="${CSS.escape(key)}"]`);
    if (field) return field;
    try {
      field = form.querySelector(`#${CSS.escape(key)}`);
      if (field) return field;
    } catch (error) {
      field = null;
    }
    const labels = form.querySelectorAll("label");
    for (const lab of labels) {
      if (lab.textContent.trim().toLowerCase().includes(String(key).toLowerCase())) {
        const id = lab.getAttribute("for");
        if (id) {
          const byId = document.getElementById(id);
          if (byId) return byId;
        }
        const inner = lab.querySelector("input, textarea, select");
        if (inner) return inner;
      }
    }
    return null;
  }

  async _submitForm(args = {}) {
    const element = this._resolveTarget(String(args.target || ""));
    const form = element.tagName === "FORM" ? element : element.closest("form") || element.form || null;
    if (!form) return { error: `no form found for '${args.target}'` };
    const filled = [];
    const fields = args.fields && typeof args.fields === "object" ? args.fields : null;
    if (fields) {
      for (const [key, value] of Object.entries(fields)) {
        const field = this._fieldInForm(form, key);
        if (!field) {
          filled.push({ field: key, error: "not found" });
          continue;
        }
        const cm = this._codeMirrorOf(field);
        if (cm) {
          cm.setValue(String(value));
          if (cm.save) cm.save();
        } else if (field.tagName.toLowerCase() === "select") {
          field.value = String(value);
          field.dispatchEvent(new Event("change", { bubbles: true }));
        } else if (field.type === "checkbox" || field.type === "radio") {
          field.checked = !!value;
          field.dispatchEvent(new Event("change", { bubbles: true }));
        } else {
          this._setNativeValue(field, String(value));
          field.dispatchEvent(new Event("input", { bubbles: true }));
          field.dispatchEvent(new Event("change", { bubbles: true }));
        }
        filled.push({ field: key });
      }
      await this._sleep(120);
    }
    const before = this._snapshot();
    const submit = form.querySelector("button[type='submit'], input[type='submit'], button:not([type])");
    if (submit) this._fireClick(submit, "left");
    else if (form.requestSubmit) form.requestSubmit();
    else form.submit();
    await this._sleep(SETTLE_MS);
    const after = this._snapshot();
    return { submitted: true, filled, ...this._delta(before, after, form) };
  }

  async _waitFor(args = {}) {
    const target = String(args.target || "");
    const condition = String(args.condition || "visible").toLowerCase();
    const text = String(args.text || "");
    const timeout = Math.min(Math.max(Number(args.timeout_ms) || 8000, 100), WAIT_CAP_MS);
    const start = Date.now();
    const deadline = start + timeout;
    while (Date.now() < deadline) {
      let element = null;
      try {
        element = this._resolveTarget(target);
      } catch (error) {
        element = null;
      }
      const visible = element && this._visible(element);
      if (condition === "hidden" && !visible) {
        return { satisfied: true, condition, waited_ms: Date.now() - start };
      }
      if (condition === "visible" && visible) {
        return { satisfied: true, condition, waited_ms: Date.now() - start };
      }
      if (condition === "text_contains" && element && (element.textContent || "").includes(text)) {
        return { satisfied: true, condition, waited_ms: Date.now() - start };
      }
      await this._sleep(WAIT_POLL_MS);
    }
    return {
      satisfied: false,
      condition,
      waited_ms: Date.now() - start,
      message: `condition '${condition}' not met for '${target}' within ${timeout}ms`,
    };
  }

  _keyCode(key) {
    if (key.length === 1) {
      if (/[a-z]/i.test(key)) return `Key${key.toUpperCase()}`;
      if (/[0-9]/.test(key)) return `Digit${key}`;
    }
    return key;
  }

  _pressKey(args = {}) {
    const key = String(args.key || "");
    if (!key) return { error: "no key provided" };
    let element = document.activeElement || document.body;
    if (args.target) element = this._resolveTarget(String(args.target));
    element.focus && element.focus();
    const code = this._keyCode(key);
    const opts = { key, code, bubbles: true, cancelable: true };
    const down = new KeyboardEvent("keydown", opts);
    element.dispatchEvent(down);
    if (key.length === 1) element.dispatchEvent(new KeyboardEvent("keypress", opts));
    element.dispatchEvent(new KeyboardEvent("keyup", opts));
    if (key === "Enter" && !down.defaultPrevented) {
      const form = element.form || (element.closest && element.closest("form"));
      if (form && element.tagName === "INPUT" && form.requestSubmit) {
        const submit = form.querySelector("button[type='submit'], input[type='submit']");
        if (submit) this._fireClick(submit, "left");
        else form.requestSubmit();
      }
    }
    return { pressed: key, target: this._briefLabel(element) };
  }

  async _runSequence(steps) {
    if (!Array.isArray(steps) || steps.length === 0) return { error: "no steps provided" };
    const blocked = new Set(["run_js", "run_sequence", "navigate_to", "reload_page", "open_terminal"]);
    const results = [];
    let failedAt = null;
    for (let i = 0; i < steps.length; i += 1) {
      const step = steps[i] || {};
      const action = String(step.action || "");
      if (!action) {
        results.push({ step: i + 1, error: "missing action" });
        failedAt = i + 1;
        break;
      }
      if (blocked.has(action)) {
        results.push({ step: i + 1, action, error: `'${action}' is not allowed inside a sequence` });
        failedAt = i + 1;
        break;
      }
      const stepArgs = { ...step };
      delete stepArgs.action;
      let result;
      try {
        result = await this._dispatch(action, stepArgs);
      } catch (error) {
        result = { error: String(error && error.message ? error.message : error) };
      }
      results.push({ step: i + 1, action, result });
      if (result && result.error) {
        failedAt = i + 1;
        break;
      }
      await this._sleep(WAIT_POLL_MS);
    }
    return {
      completed: failedAt === null ? steps.length : failedAt - 1,
      total: steps.length,
      failedAt,
      results,
      context: this._context(),
    };
  }

  _scrollTo(selector) {
    const element = this._resolveTarget(selector);
    element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
    return `scrolled to ${selector}`;
  }

  _highlight(selector, label) {
    const element = this._resolveTarget(selector);
    element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
    const box = document.createElement("div");
    box.className = "devii-hl-box";
    const callout = label ? document.createElement("div") : null;
    if (callout) {
      callout.className = "devii-hl-callout";
      callout.textContent = String(label);
    }
    document.body.appendChild(box);
    if (callout) document.body.appendChild(callout);
    const entry = { element, box, callout };
    this._highlights.push(entry);
    if (this._highlights.length === 1) {
      window.addEventListener("scroll", this._reposition, true);
      window.addEventListener("resize", this._reposition);
    }
    this._place(entry);
    return `highlighted ${selector}`;
  }

  _place(entry) {
    const rect = entry.element.getBoundingClientRect();
    const pad = 4;
    entry.box.style.left = `${rect.left - pad}px`;
    entry.box.style.top = `${rect.top - pad}px`;
    entry.box.style.width = `${rect.width + pad * 2}px`;
    entry.box.style.height = `${rect.height + pad * 2}px`;
    if (entry.callout) {
      const below = rect.bottom + 8;
      entry.callout.style.left = `${Math.max(8, rect.left)}px`;
      entry.callout.style.top = `${below}px`;
    }
  }

  _repositionHighlights() {
    this._highlights.forEach((entry) => this._place(entry));
  }

  _clearHighlights() {
    const count = this._highlights.length;
    this._highlights.forEach((entry) => {
      entry.box.remove();
      if (entry.callout) entry.callout.remove();
    });
    this._highlights = [];
    window.removeEventListener("scroll", this._reposition, true);
    window.removeEventListener("resize", this._reposition);
    return `cleared ${count} highlight(s)`;
  }

  _toast(text, duration) {
    if (!text) return { error: "no text provided" };
    const toast = document.createElement("div");
    toast.className = "devii-toast";
    toast.textContent = text;
    document.body.appendChild(toast);
    window.requestAnimationFrame(() => toast.classList.add("devii-toast-show"));
    window.setTimeout(() => {
      toast.classList.remove("devii-toast-show");
      window.setTimeout(() => toast.remove(), 250);
    }, Math.max(1000, duration));
    return "toast shown";
  }

  _navigate(url) {
    if (!url) return { error: "no url provided" };
    window.setTimeout(() => window.location.assign(url), NAV_DELAY_MS);
    return `navigating to ${url}`;
  }

  _reload() {
    window.setTimeout(() => window.location.reload(), NAV_DELAY_MS);
    return "reloading";
  }

  _openTerminal(args) {
    const projectSlug = String(args.project_slug || "").trim();
    const instance = String(args.instance || "").trim();
    const label = String(args.label || instance || "").trim();
    if (!projectSlug || !instance) return { error: "project_slug and instance are required" };
    if (!globalThis.app || !globalThis.app.containerTerminals) {
      return { error: "terminal manager unavailable" };
    }
    globalThis.app.containerTerminals.open(projectSlug, instance, label);
    return { opened: true, instance };
  }
}
