// retoor <retoor@molodetz.nl>

import FloatingWindow from "../components/FloatingWindow.js";
import { Http } from "../Http.js";
import { assetUrl } from "../assetVersion.js";
import Markdown from "./markdown.js";
import DeviiSocket from "./DeviiSocket.js";
import DeviiClient from "./DeviiClient.js";
import { UserScope } from "../userScope.js";

const CSS_ID = "devii-terminal-css";
const STORAGE_KEY = "devii-terminal-state";
const HISTORY_KEY = "devii-terminal-history";
const FONT_KEY = "devii-fontsize";
const FONT_MIN = 10;
const FONT_MAX = 26;
const FONT_DEFAULT = 14;
const HISTORY_LIMIT = 500;
const TRACE_GLYPHS = {
  call: "↳",
  ok: "✓",
  err: "✗",
  "plan-gate": "plan required first",
  "reflect-trigger": "reflecting on error",
  "verify-gate": "verification gate",
  compact: "context compaction",
};
const CLEAR_WORDS = new Set(["clear", "/clear"]);
const RESET_WORDS = new Set(["reset", "/reset"]);
const STOP_WORDS = new Set(["stop", "/stop"]);
const CLOSE_WORDS = new Set(["exit", "close", "quit", "bye", "/exit", "/close", "/quit", "/bye"]);
const HELP = [
  "Local commands:",
  "  /help               show this help",
  "  stop                cancel the running task, keep the conversation",
  "  reset               cancel the running task and clear the conversation",
  "  clear               clear the conversation",
  "  exit | close | quit | bye   close the terminal",
  "  /show               show devii (the avatar)",
  "  /hide               hide devii",
  "  /say <text>         make devii speak directly",
  "  /character <name>   switch devii's character",
  "Anything else is sent to the agent.",
].join("\n");

export default class DeviiTerminalElement extends FloatingWindow {
  constructor() {
    super();
    this.markdown = new Markdown();
    this.deviiClient = new DeviiClient();
    this.socket = null;
    this.history = [];
    this.historyIndex = 0;
    this.draft = "";
    this._historyKey = null;
    this._hasFocus = false;
    this._unloading = false;
  }

  get _dragClass() {
    return "devii-dragging";
  }

  get fontControls() {
    return true;
  }

  get _normalizeSize() {
    return [760, 600];
  }

  _fontAtMin() {
    return this._readFont() <= FONT_MIN;
  }

  _fontAtMax() {
    return this._readFont() >= FONT_MAX;
  }

  connectedCallback() {
    this._ensureCss();
    this._buildChrome();
    this._registerWindow();
    this._applyFont(this._readFont());
    this._mobile.addEventListener("change", () => this._onViewportChange());
    window.addEventListener("resize", () => this._clampWindow());
    this._vv = window.visualViewport || null;
    this._vvHandler = () => this._onVisualViewport2();
    if (this._vv) {
      this._vv.addEventListener("resize", this._vvHandler);
      this._vv.addEventListener("scroll", this._vvHandler);
    }
    window.addEventListener("pagehide", () => this._captureFocusForUnload());
    window.addEventListener("beforeunload", () => this._captureFocusForUnload());
    this._identity = null;
    this._fetchIdentity().then((id) => {
      this._identity = id;
      this._initHistory(id);
    });
    window.addEventListener("focus", () => {
      this._checkIdentity();
      this._reportVisibility();
    });
    window.addEventListener("blur", () => this._reportVisibility());
    document.addEventListener("visibilitychange", () => {
      if (!document.hidden) this._checkIdentity();
      this._reportVisibility();
    });
    this.socket = new DeviiSocket({
      onOpen: () => {
        this._reportVisibility();
        this._reportClientInfo();
      },
      onReady: () => {
        this._notice("connected.");
        this._reportVisibility();
        this._reportClientInfo();
      },
      onClose: () => this._notice("disconnected, reconnecting..."),
      onMessage: (message) => this._onMessage(message),
    });
    this._connected = false;
    this._restoring = true;
    const saved = this._loadPersisted();
    if (saved && saved.geometry) {
      this.geometry = saved.geometry;
      this._clampGeometry();
    }
    let initial = "closed";
    if (saved && saved.state) {
      initial = saved.state;
    } else if (this.hasAttribute("open")) {
      initial = "open";
    }
    this._restoring = false;
    this._setState(initial);
    this._observeResize();
    if (this.state !== "closed") {
      this._ensureConnected();
      this._focusInput();
    }
  }

  _focusInput() {
    window.requestAnimationFrame(() => {
      try {
        this.input.focus({ preventScroll: true });
      } catch (error) {
        return;
      }
    });
  }

  _onFocusChange(focused) {
    if (!focused && this._unloading) return;
    this._hasFocus = focused;
    this._persist();
  }

  _captureFocusForUnload() {
    this._unloading = true;
    this._hasFocus = document.activeElement === this.input;
    this._persist();
  }

  hasStoredState() {
    try {
      return window.localStorage.getItem(STORAGE_KEY) !== null;
    } catch (error) {
      return false;
    }
  }

  _loadPersisted() {
    try {
      const raw = window.localStorage.getItem(STORAGE_KEY);
      return raw ? JSON.parse(raw) : null;
    } catch (error) {
      return null;
    }
  }

  _persist() {
    if (this._restoring) return;
    try {
      window.localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify({ state: this.state, geometry: this.geometry, focused: this._hasFocus }),
      );
    } catch (error) {
      return;
    }
  }

  _persistGeometry() {
    this._persist();
  }

  _historyKeyFor(identity) {
    const scope = identity && identity.loggedIn && identity.username
      ? `user:${identity.username}`
      : "guest";
    return `${HISTORY_KEY}:${scope}`;
  }

  _initHistory(identity) {
    this._historyKey = this._historyKeyFor(identity);
    this.history = this._loadHistory();
    this.historyIndex = this.history.length;
    this.draft = "";
  }

  _loadHistory() {
    if (!this._historyKey) return [];
    try {
      const raw = window.localStorage.getItem(this._historyKey);
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
    } catch (error) {
      return [];
    }
  }

  _saveHistory() {
    if (!this._historyKey) return;
    try {
      window.localStorage.setItem(this._historyKey, JSON.stringify(this.history));
    } catch (error) {
      return;
    }
  }

  _ensureConnected() {
    if (this._connected) return;
    this._connected = true;
    this.socket.connect();
  }

  _ensureCss() {
    if (document.getElementById(CSS_ID)) return;
    const link = document.createElement("link");
    link.id = CSS_ID;
    link.rel = "stylesheet";
    link.href = assetUrl("/static/css/devii.css");
    document.head.appendChild(link);
  }

  _buildChrome() {
    this.innerHTML = "";
    this.launcher = document.createElement("button");
    this.launcher.type = "button";
    this.launcher.className = "devii-launcher";
    this.launcher.innerHTML = '<span class="devii-launcher-dot"></span> devii';
    this.launcher.addEventListener("click", () => this.open());
    this.appendChild(this.launcher);

    this.win = document.createElement("div");
    this.win.className = "devii-window";
    this.win.setAttribute("role", "dialog");
    this.win.setAttribute("aria-label", "Devii assistant");
    this.win.innerHTML = [
      '<div class="devii-titlebar">',
      '  <span class="devii-title"><span class="devii-dot" aria-hidden="true"></span>Devii</span>',
      '  <span class="devii-winctl">',
      '    <button type="button" data-win="font-dec" title="Smaller font">&#8722;</button>',
      '    <button type="button" data-win="font-inc" title="Larger font">&#43;</button>',
      '    <button type="button" data-win="minimize" title="Minimize (smallest usable size)">&#9643;</button>',
      '    <button type="button" data-win="normalize" title="Normalize (comfortable size)">&#9645;</button>',
      '    <button type="button" data-win="fullscreen" title="Fullscreen">&#9974;</button>',
      '    <button type="button" data-win="maximize" title="Maximize">&#9633;</button>',
      '    <button type="button" data-win="close" title="Close">&#10005;</button>',
      "  </span>",
      "</div>",
      '<div class="devii-toolbar">',
      '  <button type="button" data-cmd="/show">show devii</button>',
      '  <button type="button" data-cmd="/hide">hide devii</button>',
      '  <button type="button" data-cmd="/clear">clear</button>',
      "</div>",
      '<div class="devii-output" role="log" aria-live="polite" aria-label="Conversation"></div>',
      '<div class="devii-inputrow">',
      '  <span class="devii-prompt" aria-hidden="true">you&gt;</span>',
      '  <input class="devii-input" type="text" autocomplete="off" spellcheck="false" aria-label="Message Devii" />',
      "</div>",
    ].join("");
    this.appendChild(this.win);

    this.output = this.win.querySelector(".devii-output");
    this.input = this.win.querySelector(".devii-input");
    this.titlebar = this.win.querySelector(".devii-titlebar");

    this.win.querySelectorAll(".devii-toolbar button").forEach((button) => {
      button.addEventListener("click", () => this._submit(button.dataset.cmd));
    });
    this.win.querySelectorAll(".devii-winctl button").forEach((button) => {
      if (button.title) button.setAttribute("aria-label", button.title);
      button.addEventListener("click", () => this._window(button.dataset.win));
    });
    this.input.addEventListener("keydown", (event) => this._onKey(event));
    this.input.addEventListener("focus", () => this._onFocusChange(true));
    this.input.addEventListener("blur", () => this._onFocusChange(false));
    this.win.addEventListener("click", (event) => {
      if (event.target.closest("button, a, input, textarea")) return;
      const selection = window.getSelection();
      if (selection && selection.toString().length > 0) return;
      this.input.focus({ preventScroll: true });
    });
    this.output.addEventListener("click", (event) => this._onOutputClick(event));
    this.titlebar.addEventListener("mousedown", (event) => this._startDrag(event));
    this.titlebar.addEventListener("touchstart", (event) => this._startDrag(event), { passive: false });
    this.titlebar.addEventListener("dblclick", (event) => {
      if (event.target.closest("button")) return;
      this._window("maximize");
    });
  }

  open() {
    this._ensureConnected();
    this._setState("open");
    this.input.focus({ preventScroll: true });
  }

  prefill(text) {
    if (!text) return;
    this.input.value = text;
    this.input.setSelectionRange(text.length, text.length);
    this.input.focus({ preventScroll: true });
  }

  close() {
    this._setState("closed");
  }

  toggle() {
    if (this.state === "closed") this.open();
    else this.close();
  }

  _contextItems() {
    const selection = window.getSelection ? String(window.getSelection()) : "";
    return [
      { label: "Copy", disabled: !selection && !this.input.value, onSelect: () => this._copySelection() },
      { label: "Paste", onSelect: () => this._paste() },
      { separator: true },
      { label: "Minimize", onSelect: () => this.minimizeWindow() },
      { label: "Normalize", onSelect: () => this.normalizeWindow() },
      { separator: true },
      { label: "Close", danger: true, onSelect: () => this.close() },
    ];
  }

  _copySelection() {
    const selection = window.getSelection ? String(window.getSelection()) : "";
    const text = selection || this.input.value;
    if (!text) return;
    this._clipboardWrite(text);
  }

  async _paste() {
    if (!navigator.clipboard || !navigator.clipboard.readText) {
      this._error("clipboard read unavailable.");
      return;
    }
    let text = "";
    try {
      text = await navigator.clipboard.readText();
    } catch (error) {
      this._error("paste failed.");
      return;
    }
    this._insertInput(text);
  }

  _insertInput(text) {
    if (!text) return;
    const start = this.input.selectionStart != null ? this.input.selectionStart : this.input.value.length;
    const end = this.input.selectionEnd != null ? this.input.selectionEnd : this.input.value.length;
    this.input.value = this.input.value.slice(0, start) + text + this.input.value.slice(end);
    const caret = start + text.length;
    this.input.focus({ preventScroll: true });
    this.input.setSelectionRange(caret, caret);
  }

  _clipboardWrite(text) {
    if (!text) return;
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).catch(() => this._fallbackCopy(text, () => {}));
    } else {
      this._fallbackCopy(text, () => {});
    }
  }

  _fontKey() {
    return `${FONT_KEY}:${UserScope.get()}`;
  }

  _readFont() {
    try {
      const value = parseInt(window.localStorage.getItem(this._fontKey()), 10);
      return Number.isFinite(value) ? Math.max(FONT_MIN, Math.min(FONT_MAX, value)) : FONT_DEFAULT;
    } catch (error) {
      return FONT_DEFAULT;
    }
  }

  _applyFont(size) {
    this.style.setProperty("--devii-font-size", `${size}px`);
  }

  _changeFont(delta) {
    const next = Math.max(FONT_MIN, Math.min(FONT_MAX, this._readFont() + delta));
    try {
      window.localStorage.setItem(this._fontKey(), String(next));
    } catch (error) {
      return this._applyFont(next);
    }
    this._applyFont(next);
  }

  _setState(state) {
    let resolved = state;
    if (state === "open") {
      resolved = this._mobile.matches ? "fullscreen" : "normal";
    } else if (state !== "closed" && this._mobile.matches) {
      resolved = "fullscreen";
    }
    this.state = resolved;
    this.classList.toggle("devii-mobile", this._mobile.matches);
    ["closed", "normal", "maximized", "fullscreen"].forEach((name) => {
      this.classList.toggle(`devii-state-${name}`, name === resolved);
    });
    if (resolved === "normal") {
      this._applyGeometry();
    } else {
      this.win.style.left = "";
      this.win.style.top = "";
      this.win.style.width = "";
      this.win.style.height = "";
    }
    const keyboardVisible = this._keyboardVisible;
    this.classList.toggle("devii-keyboard-visible", keyboardVisible);
    if (resolved === "fullscreen" && keyboardVisible && this._vv) {
      this.win.style.height = `${this._vv.height}px`;
      this.win.style.top = `${this._vv.offsetTop}px`;
    }
    this._syncControls();
    this._persist();
  }

  _defaultGeometry() {
    const safeHeight = this._safeViewport.height;
    const width = Math.min(760, Math.round(window.innerWidth * 0.94));
    const height = Math.min(600, Math.round(safeHeight * 0.82));
    return {
      width,
      height,
      left: Math.max(12, window.innerWidth - width - 24),
      top: Math.max(12, safeHeight - height - 24),
    };
  }

  _onViewportChange() {
    if (this.state === "closed") return;
    this._setState("open");
  }

  _onVisualViewport2() {
    if (this.state === "closed" || !this._vv) return;
    this.classList.toggle("devii-keyboard-visible", this._keyboardVisible);
    if (this.state === "fullscreen") {
      this.win.style.height = `${this._vv.height}px`;
      this.win.style.top = `${this._vv.offsetTop}px`;
    }
    this._ensureInputVisible();
  }

  _ensureInputVisible() {
    window.requestAnimationFrame(() => {
      if (document.activeElement === this.input && this.input.scrollIntoView) {
        this.input.scrollIntoView({ block: "end" });
      }
      if (this.output) this.output.scrollTop = this.output.scrollHeight;
    });
  }

  _onKey(event) {
    if (event.key === "Enter") {
      event.preventDefault();
      const text = this.input.value;
      this.input.value = "";
      this._submit(text);
    } else if (event.key === "ArrowUp" || event.key === "PageUp") {
      event.preventDefault();
      this._recall(-1);
    } else if (event.key === "ArrowDown" || event.key === "PageDown") {
      event.preventDefault();
      this._recall(1);
    } else if (event.key === "Escape") {
      this.close();
    }
  }

  _recall(direction) {
    if (!this.history.length) return;
    if (this.historyIndex === this.history.length) {
      this.draft = this.input.value;
    }
    const target = Math.max(0, Math.min(this.history.length, this.historyIndex + direction));
    if (target === this.historyIndex) return;
    this.historyIndex = target;
    const value = target === this.history.length ? this.draft : this.history[target];
    this.input.value = value;
    this.input.setSelectionRange(value.length, value.length);
  }

  _submit(raw) {
    const text = (raw || "").trim();
    if (!text) return;
    if (this.history[this.history.length - 1] !== text) {
      this.history.push(text);
      if (this.history.length > HISTORY_LIMIT) {
        this.history = this.history.slice(-HISTORY_LIMIT);
      }
      this._saveHistory();
    }
    this.historyIndex = this.history.length;
    this.draft = "";
    const lowered = text.toLowerCase();
    if (STOP_WORDS.has(lowered)) {
      this._stopTask();
      return;
    }
    if (RESET_WORDS.has(lowered)) {
      this._resetConversation();
      return;
    }
    if (CLEAR_WORDS.has(lowered)) {
      this._clearConversation();
      return;
    }
    if (CLOSE_WORDS.has(lowered)) {
      this.close();
      return;
    }
    if (text.startsWith("/")) {
      this._local(text);
      return;
    }
    this._userLine(text);
    if (!this.socket.send({ type: "input", text })) {
      this._error("not connected.");
    }
  }

  _stopTask() {
    this.socket.send({ type: "stop" });
  }

  _resetConversation() {
    this.output.innerHTML = "";
    this.socket.send({ type: "reset" });
  }

  _clearConversation() {
    this.output.innerHTML = "";
    this.socket.send({ type: "clear" });
  }

  async _local(text) {
    const [command, ...rest] = text.slice(1).split(" ");
    const argument = rest.join(" ").trim();
    const avatar = this._avatar();
    if (command === "help") {
      this._agent(HELP);
    } else if (command === "clear") {
      this.output.innerHTML = "";
    } else if (command === "show") {
      this._userLine(text);
      if (avatar) await avatar.execute("show", {});
    } else if (command === "hide") {
      this._userLine(text);
      if (avatar) await avatar.execute("hide", {});
    } else if (command === "say") {
      this._userLine(text);
      if (avatar) await avatar.execute("speak", { text: argument });
    } else if (command === "character") {
      this._userLine(text);
      if (avatar) await avatar.execute("switch_character", { name: argument });
    } else {
      this._error(`unknown command: /${command} (try /help)`);
    }
  }

  async _onMessage(message) {
    if (message.type === "reply") {
      this._agent(message.text);
    } else if (message.type === "history") {
      this._renderHistory(message.messages);
    } else if (message.type === "clear") {
      this.output.innerHTML = "";
    } else if (message.type === "status") {
      this._notice(message.text);
    } else if (message.type === "trace") {
      this._trace(message);
    } else if (message.type === "error") {
      this._error(message.text);
    } else if (message.type === "task") {
      this._task(message);
    } else if (message.type === "avatar" || message.type === "avatar_query") {
      await this._avatarRequest(message);
    } else if (message.type === "client") {
      await this._clientRequest(message);
    } else if (message.type === "interaction") {
      await this._interactionRequest(message);
    } else if (message.type === "interaction_closed") {
      this._closeOpenInteraction(message.result || message);
    } else if (message.type === "auth") {
      this._onAuth(message);
    }
  }

  _onAuth(message) {
    if (message.action === "logout") {
      window.location.assign("/auth/logout");
    } else if (message.action === "adopt") {
      const next = location.pathname + location.search;
      window.location.assign("/devii/adopt?next=" + encodeURIComponent(next));
    }
  }

  async _fetchIdentity() {
    try {
      const data = await Http.getJson("/devii/session");
      if (data.baseUrl) {
        this.markdown.baseUrl = data.baseUrl;
      }
      return { loggedIn: !!data.loggedIn, username: data.username || "" };
    } catch (error) {
      return null;
    }
  }

  async _checkIdentity() {
    if (this._checkingIdentity || !this._identity) return;
    this._checkingIdentity = true;
    try {
      const now = await this._fetchIdentity();
      if (now && (now.loggedIn !== this._identity.loggedIn || now.username !== this._identity.username)) {
        window.location.reload();
      }
    } finally {
      this._checkingIdentity = false;
    }
  }

  _reportVisibility() {
    if (!this.socket) return;
    this.socket.send({
      type: "visibility",
      visible: !document.hidden,
      focused: document.hasFocus(),
    });
  }

  _reportClientInfo() {
    if (!this.socket) return;
    let timezone = "";
    try {
      timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
    } catch (error) {
      timezone = "";
    }
    this.socket.send({
      type: "clientinfo",
      timezone,
      offset: -new Date().getTimezoneOffset(),
    });
  }

  async _clientRequest(message) {
    let result;
    try {
      result = await this.deviiClient.execute(message.action, message.args || {});
    } catch (error) {
      result = { error: String(error && error.message ? error.message : error) };
    }
    this.socket.send({ type: "client_result", id: message.id, result });
  }

  async _interactionRequest(message) {
    const args = message.args || {};
    const interactionId = args.interaction_id || message.interaction_id || "";
    try {
      const result = await this._mountInteraction(args, interactionId);
      this.socket.send({
        type: "interaction_result",
        id: message.id,
        interaction_id: interactionId,
        result,
      });
    } catch (error) {
      this.socket.send({
        type: "interaction_result",
        id: message.id,
        interaction_id: interactionId,
        result: {
          status: "error",
          interaction_id: interactionId,
          values: {},
          error: String(error && error.message ? error.message : error),
        },
      });
    }
  }

  async _mountInteraction(args, interactionId) {
    const root = document.createElement("ai-interaction");
    root.id = interactionId || `ix-${Date.now()}`;
    root.setAttribute("channel", args.channel || "site-chat");
    root.setAttribute("state", "open");
    root.setAttribute("title", args.title || "Question");
    root.setAttribute("submit-label", args.submit_label || "Confirm");
    root.setAttribute("cancel-label", args.cancel_label || "Cancel");
    if (args.cancelable !== false) root.setAttribute("cancelable", "");
    if (args.timeout_sec) root.setAttribute("timeout-sec", String(args.timeout_sec));

    if (args.description) {
      const help = document.createElement("ai-help");
      help.setAttribute("text", args.description);
      root.appendChild(help);
    }

    for (const widget of args.widgets || []) {
      const el = this._widgetElement(widget);
      if (el) root.appendChild(el);
    }

    const line = document.createElement("div");
    line.className = "devii-line devii-agent devii-interaction-line";
    line.appendChild(root);
    this._append(line);

    const tags = new Set(
      Array.from(root.querySelectorAll("*"))
        .map((el) => el.localName)
        .filter((name) => name && name.startsWith("ai-"))
    );
    tags.add("ai-interaction");
    if (window.app && window.app.aiAutoload) {
      window.app.aiAutoload.scan(root);
      await Promise.all(
        Array.from(tags).map((tag) =>
          window.app.aiAutoload.ensure(tag).catch(() => null)
        )
      );
    }
    await Promise.all(
      Array.from(tags).map((tag) => {
        if (customElements.get(tag)) return Promise.resolve();
        return Promise.race([
          customElements.whenDefined(tag),
          new Promise((resolve) => window.setTimeout(resolve, 2500)),
        ]).catch(() => null);
      })
    );
    const missing = Array.from(tags).filter((tag) => !customElements.get(tag));
    if (missing.length) {
      console.error("Devii interaction widgets not defined:", missing.join(", "));
    }
    await new Promise((resolve) => {
      window.requestAnimationFrame(() => window.requestAnimationFrame(resolve));
    });
    this._revealInteraction(root);

    return new Promise((resolve) => {
      let settled = false;
      const finish = (status, values, error = null, meta = null) => {
        if (settled) return;
        settled = true;
        if (this._interactionReveal) {
          this._interactionReveal.disconnect();
          this._interactionReveal = null;
        }
        if (this._openInteraction && this._openInteraction.id === interactionId) {
          this._openInteraction = null;
        }
        if (root && typeof root._close === "function") {
          root._close(status === "submitted" ? "submitted" : status);
        } else if (root) {
          root.setAttribute("state", status);
        }
        resolve({
          status,
          interaction_id: interactionId,
          values: values || {},
          error,
          meta: meta || { adapter: "site-chat", degraded: false },
        });
      };

      this._openInteraction = {
        id: interactionId,
        requestId: `ix:${interactionId}`,
        root,
        finish,
        args,
      };

      root.addEventListener("ai:submit", (event) => {
        finish("submitted", (event.detail && event.detail.values) || {});
      });
      root.addEventListener("ai:cancel", () => {
        finish("cancelled", {});
      });
      root.addEventListener("ai:timeout", () => {
        finish("timeout", {});
      });
    });
  }

  _closeOpenInteraction(result) {
    const open = this._openInteraction;
    if (!open || typeof open.finish !== "function") return;
    const payload = result && typeof result === "object" ? result : {};
    open.finish(
      payload.status || "submitted",
      payload.values || {},
      payload.error || null,
      payload.meta || { adapter: "site-chat", via: "text", degraded: true }
    );
  }

  _revealInteraction(root) {
    if (!root || !this.output) return;
    const scrollToEnd = () => {
      const actions = root.querySelector("ai-actions");
      const target = actions || root;
      try {
        target.scrollIntoView({ block: "end", inline: "nearest", behavior: "instant" });
      } catch (error) {
        this.output.scrollTop = this.output.scrollHeight;
      }
      const rect = root.getBoundingClientRect();
      const box = this.output.getBoundingClientRect();
      if (rect.bottom > box.bottom - 8) {
        this.output.scrollTop += rect.bottom - box.bottom + 16;
      }
      if (rect.top < box.top + 8 && root.offsetHeight <= this.output.clientHeight - 24) {
        this.output.scrollTop -= box.top + 8 - rect.top;
      }
    };
    scrollToEnd();
    if (typeof ResizeObserver === "undefined") return;
    if (this._interactionReveal) this._interactionReveal.disconnect();
    let frames = 0;
    this._interactionReveal = new ResizeObserver(() => {
      frames += 1;
      scrollToEnd();
      if (frames >= 6 && this._interactionReveal) {
        this._interactionReveal.disconnect();
        this._interactionReveal = null;
      }
    });
    this._interactionReveal.observe(root);
  }

  _widgetElement(widget) {
    if (!widget || typeof widget !== "object") return null;
    const type = widget.type;
    if (type === "//") {
      const help = document.createElement("ai-help");
      help.setAttribute("text", widget.text || widget.label || "");
      return help;
    }
    if (type === "group") {
      const group = document.createElement("ai-group");
      group.setAttribute("label", widget.label || "Group");
      for (const child of widget.widgets || []) {
        const el = this._widgetElement(child);
        if (el) group.appendChild(el);
      }
      return group;
    }
    if (type === "confirm") {
      const el = document.createElement("ai-confirm");
      el.setAttribute("name", widget.name || "confirm");
      el.setAttribute("label", widget.label || "Confirm");
      if (widget.help) el.setAttribute("help", widget.help);
      if (widget.default === true || widget.default === false) {
        el.setAttribute("default", String(widget.default));
      }
      if (widget.required !== false) el.setAttribute("required", "");
      return el;
    }
    if (type === "choice" || type === "select") {
      const el = document.createElement(type === "select" ? "ai-select" : "ai-choice");
      el.setAttribute("name", widget.name || "choice");
      el.setAttribute("label", widget.label || "Choice");
      if (widget.help) el.setAttribute("help", widget.help);
      if (widget.display) el.setAttribute("display", widget.display);
      if (widget.default != null) el.setAttribute("value", String(widget.default));
      if (widget.required !== false) el.setAttribute("required", "");
      for (const option of widget.options || []) {
        const opt = document.createElement("ai-option");
        opt.setAttribute("value", option.value || "");
        opt.setAttribute("label", option.label || option.value || "");
        if (option.description) opt.setAttribute("description", option.description);
        if (option.disabled) opt.setAttribute("disabled", "");
        if (widget.default != null && String(widget.default) === String(option.value)) {
          opt.setAttribute("selected", "");
        }
        el.appendChild(opt);
      }
      return el;
    }
    if (type === "choice_multi") {
      const el = document.createElement("ai-choice-multi");
      el.setAttribute("name", widget.name || "choices");
      el.setAttribute("label", widget.label || "Choices");
      if (widget.help) el.setAttribute("help", widget.help);
      if (widget.required !== false) el.setAttribute("required", "");
      const defaults = Array.isArray(widget.default) ? widget.default.map(String) : [];
      for (const option of widget.options || []) {
        const opt = document.createElement("ai-option");
        opt.setAttribute("value", option.value || "");
        opt.setAttribute("label", option.label || option.value || "");
        if (option.description) opt.setAttribute("description", option.description);
        if (defaults.includes(String(option.value))) opt.setAttribute("selected", "");
        el.appendChild(opt);
      }
      return el;
    }
    if (type === "text" || type === "number" || type === "date") {
      const el = document.createElement("ai-field");
      el.setAttribute("name", widget.name || "field");
      el.setAttribute("type", type);
      el.setAttribute("label", widget.label || widget.name || "Field");
      if (type === "date") {
        el.setAttribute("help", widget.help || "Format: DD/MM/YYYY");
        el.setAttribute("placeholder", widget.placeholder || "DD/MM/YYYY");
      } else if (widget.help) {
        el.setAttribute("help", widget.help);
      }
      if (widget.placeholder && type !== "date") {
        el.setAttribute("placeholder", widget.placeholder);
      }
      if (widget.max_length != null) el.setAttribute("maxlength", String(widget.max_length));
      if (widget.min != null) el.setAttribute("min", String(widget.min));
      if (widget.max != null) el.setAttribute("max", String(widget.max));
      if (widget.step != null) el.setAttribute("step", String(widget.step));
      if (widget.pattern) el.setAttribute("pattern", widget.pattern);
      if (widget.default != null) el.setAttribute("value", String(widget.default));
      if (widget.required !== false) el.setAttribute("required", "");
      return el;
    }
    return null;
  }

  async _avatarRequest(message) {
    const avatar = this._avatar();
    let result = { error: "no avatar attached" };
    if (avatar) {
      try {
        result = await avatar.execute(message.action, message.args || {});
      } catch (error) {
        result = { error: String(error && error.message ? error.message : error) };
      }
    }
    this.socket.send({ type: "avatar_result", id: message.id, result });
  }

  _avatar() {
    if (this._avatarEl && this._avatarEl.isConnected) return this._avatarEl;
    const selector = this.getAttribute("avatar");
    if (selector) {
      const found = document.getElementById(selector) || document.querySelector(selector);
      if (found) {
        this._avatarEl = found;
        return found;
      }
    }
    let element = document.querySelector("devii-avatar");
    if (!element) {
      element = document.createElement("devii-avatar");
      if (selector && /^[a-zA-Z][\w-]*$/.test(selector)) element.id = selector;
      document.body.appendChild(element);
    }
    this._avatarEl = element;
    return element;
  }

  _append(node) {
    const pinned = this._atBottom();
    this.output.appendChild(node);
    this.output.scrollTop = this.output.scrollHeight;
    this._scrollOnMediaLoad(node, pinned);
  }

  _atBottom() {
    const slack = 40;
    return this.output.scrollHeight - this.output.scrollTop - this.output.clientHeight <= slack;
  }

  _scrollOnMediaLoad(node, pinned) {
    const images = node.querySelectorAll ? node.querySelectorAll("img") : [];
    images.forEach((image) => {
      if (image.complete) return;
      const settle = () => {
        if (pinned) this.output.scrollTop = this.output.scrollHeight;
      };
      image.addEventListener("load", settle, { once: true });
      image.addEventListener("error", settle, { once: true });
    });
  }

  _onOutputClick(event) {
    const messageButton = event.target.closest(".devii-copy");
    if (messageButton) {
      this._copyText(messageButton._source || "", messageButton);
      return;
    }
    const button = event.target.closest(".md-copy");
    if (!button) return;
    const wrap = button.closest(".md-codewrap");
    const code = wrap && wrap.querySelector("code");
    if (!code) return;
    this._copyText(code.textContent, button);
  }

  _attachCopy(line, source) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "devii-copy";
    button.title = "Copy markdown source";
    button.textContent = "Copy";
    button._source = source;
    line.appendChild(button);
  }

  _copyText(text, button) {
    const label = button.textContent;
    const done = () => {
      button.textContent = "Copied";
      button.classList.add("md-copied");
      window.setTimeout(() => {
        button.textContent = label;
        button.classList.remove("md-copied");
      }, 1200);
    };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(done, () => this._fallbackCopy(text, done));
    } else {
      this._fallbackCopy(text, done);
    }
  }

  _fallbackCopy(text, done) {
    const area = document.createElement("textarea");
    area.value = text;
    area.style.position = "fixed";
    area.style.opacity = "0";
    document.body.appendChild(area);
    area.select();
    try {
      document.execCommand("copy");
      done();
    } catch (error) {
      return;
    }
    document.body.removeChild(area);
  }

  _userLine(text) {
    const line = document.createElement("div");
    line.className = "devii-line devii-user";
    line.textContent = `you> ${text}`;
    this._attachCopy(line, text);
    this._append(line);
  }

  _renderHistory(messages) {
    this.output.innerHTML = "";
    (messages || []).forEach((message) => {
      const content = this._normalizeText(message && message.content);
      if (!content) return;
      if (message.role === "user") {
        this._userLine(content);
      } else if (message.role === "assistant") {
        this._agent(content);
      }
    });
  }

  _normalizeText(text) {
    let value = String(text == null ? "" : text);
    while (value.indexOf("\\\\n") !== -1) {
      value = value.split("\\\\n").join("\\n");
    }
    while (value.indexOf("\\\\t") !== -1) {
      value = value.split("\\\\t").join("\\t");
    }
    const escapedN = (value.match(/\\n/g) || []).length;
    const realN = (value.match(/\n/g) || []).length;
    if (escapedN > realN) {
      value = value.split("\\n").join("\n");
    }
    const escapedT = (value.match(/\\t/g) || []).length;
    const realT = (value.match(/\t/g) || []).length;
    if (escapedT > realT) {
      value = value.split("\\t").join("\t");
    }
    return value;
  }

  _agent(text) {
    const source = this._normalizeText(text);
    const block = document.createElement("div");
    block.className = "devii-line devii-agent";
    block.innerHTML = this.markdown.render(source);
    this._attachCopy(block, source);
    this._append(block);
  }

  _notice(text) {
    const line = document.createElement("div");
    line.className = "devii-line devii-notice";
    line.textContent = text;
    this._append(line);
  }

  _error(text) {
    const line = document.createElement("div");
    line.className = "devii-line devii-error";
    line.textContent = `[error] ${text}`;
    this._append(line);
  }

  _task(message) {
    const verbs = { start: "running...", done: "finished", finished: "finished", error: "failed" };
    const head = document.createElement("div");
    head.className = "devii-line devii-task";
    head.textContent = `[task ${message.label}] ${verbs[message.kind] || message.kind}`;
    this._append(head);
    if (message.payload && message.kind !== "start") {
      this._agent(message.payload);
    }
  }

  _trace(message) {
    const glyph = TRACE_GLYPHS[message.event] || message.event;
    const line = document.createElement("div");
    line.className = "devii-line devii-trace";
    if (message.event === "call" || message.event === "ok" || message.event === "err") {
      const detail = message.detail ? ` ${message.detail}` : "";
      line.textContent = `  ${glyph} ${message.name}${detail}`;
    } else {
      line.textContent = `  ! ${glyph}`;
    }
    this._append(line);
  }
}

customElements.define("devii-terminal", DeviiTerminalElement);
