/* global window */
// AgentUI common UI utilities (DRY helpers shared by editor.html and pm-ui.js)
(function (w) {
  'use strict';

  const AU = {};

  // HTML escaping for safe text/attribute insertion
  AU.escapeHtml = function escapeHtml(s) {
    const str = String(s ?? '');
    return str
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, "'");
  };

  // Attribute-safe escape (keeps quotes escaped; conservative)
  AU.escAttr = function escAttr(v) {
    const s = String(v ?? '');
    return s
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, "'");
  };

  // Text-node escape (keeps quotes as-is for readability)
  AU.escText = function escText(v) {
    const s = String(v ?? '');
    return s
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>');
  };

  // DRY helper: sync Drawflow node data + mirror into DOM.__data with deep copy
  AU.updateNodeDataAndDom = function updateNodeDataAndDom(editor, id, data) {
    try { editor && typeof editor.updateNodeDataFromId === 'function' && editor.updateNodeDataFromId(id, data); } catch (_) {}
    try {
      const el = document.querySelector('#node-' + id);
      if (el) el.__data = JSON.parse(JSON.stringify(data));
    } catch (_) {}
  };

  // Double rAF helper: waits for two animation frames; returns Promise or accepts callback
  AU.nextRaf2 = function nextRaf2(cb) {
    try {
      if (typeof requestAnimationFrame === 'function') {
        if (typeof cb === 'function') {
          requestAnimationFrame(() => { requestAnimationFrame(() => { try { cb(); } catch (_) {} }); });
          return;
        }
        return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
      } else {
        if (typeof cb === 'function') { setTimeout(() => { try { cb(); } catch (_) {} }, 32); return; }
        return new Promise((resolve) => setTimeout(resolve, 32));
      }
    } catch (_) {
      if (typeof cb === 'function') { try { cb(); } catch (__ ) {} }
      return Promise.resolve();
    }
  };

  // Heuristic: looks like long base64 payload
  AU.isProbablyBase64 = function isProbablyBase64(s) {
    try {
      if (typeof s !== 'string') return false;
      if (s.length < 64) return false;
      return /^[A-Za-z0-9+/=\r\n]+$/.test(s);
    } catch { return false; }
  };

  AU.trimBase64 = function trimBase64(s, maxLen = 180) {
    try {
      const str = String(s ?? '');
      if (str.length > maxLen) {
        return str.slice(0, maxLen) + `... (trimmed ${str.length - maxLen})`;
      }
      return str;
    } catch { return String(s ?? ''); }
  };

  // Flatten JSON-like object into [path, stringValue] pairs
  // Includes special handling for backend preview objects: { "__truncated__": true, "preview": "..." }
  AU.flattenObject = function flattenObject(obj, prefix = '') {
    const out = [];
    if (obj == null) return out;
    if (typeof obj !== 'object') {
      out.push([prefix, String(obj)]);
      return out;
    }
    try {
      const entries = Object.entries(obj);
      for (const [k, v] of entries) {
        const p = prefix ? `${prefix}.${k}` : k;
        if (v && typeof v === 'object' && !Array.isArray(v)) {
          // Special preview shape from backend
          if (Object.prototype.hasOwnProperty.call(v, '__truncated__') && Object.prototype.hasOwnProperty.call(v, 'preview')) {
            out.push([p, String(v.preview ?? '')]);
            continue;
          }
          out.push(...AU.flattenObject(v, p));
        } else {
          try {
            const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 0);
            out.push([p, s]);
          } catch {
            out.push([p, String(v)]);
          }
        }
      }
    } catch {
      // Fallback best-effort
      try { out.push([prefix, JSON.stringify(obj)]); } catch { out.push([prefix, String(obj)]); }
    }
    return out;
  };

  // Format headers dictionary into text lines "Key: Value"
  AU.fmtHeaders = function fmtHeaders(h) {
    try {
      const keys = Object.keys(h || {});
      return keys.map(k => `${k}: ${String(h[k])}`).join('\n');
    } catch { return ''; }
  };

  // Build HTTP request preview text
  AU.buildReqText = function buildReqText(x) {
    if (!x) return '';
    const head = `${x.method || 'POST'} ${x.url || '/'} HTTP/1.1`;
    const host = (() => {
      try { const u = new URL(x.url); return `Host: ${u.host}`; } catch { return ''; }
    })();
    const hs = AU.fmtHeaders(x.headers || {});
    const body = String(x.body_text || '').trim();
    return [head, host, hs, '', body].filter(Boolean).join('\n');
  };

  // Build HTTP response preview text
  AU.buildRespText = function buildRespText(x) {
    if (!x) return '';
    const head = `HTTP/1.1 ${x.status || 0}`;
    const hs = AU.fmtHeaders(x.headers || {});
    const body = String(x.body_text || '').trim();
    return [head, hs, '', body].filter(Boolean).join('\n');
  };

  // Unified fetch helper with timeout and JSON handling
  AU.apiFetch = async function apiFetch(url, opts) {
    const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    const o = opts || {};
    const method = String(o.method || 'GET').toUpperCase();
    const expectJson = (o.expectJson !== false); // default true
    const headers = Object.assign({}, o.headers || {});
    let body = o.body;
    const timeoutMs = Number.isFinite(o.timeoutMs) ? o.timeoutMs : 15000;

    const hasAbort = (typeof AbortController !== 'undefined');
    const ctrl = hasAbort ? new AbortController() : null;
    let to = null;
    if (ctrl) {
      try { to = setTimeout(() => { try { ctrl.abort(); } catch(_){} }, timeoutMs); } catch(_) {}
    }

    try {
      if (expectJson) {
        if (!headers['Accept'] && !headers['accept']) headers['Accept'] = 'application/json';
      }
      if (body != null) {
        const isForm = (typeof FormData !== 'undefined' && body instanceof FormData);
        const isBlob = (typeof Blob !== 'undefined' && body instanceof Blob);
        if (typeof body === 'object' && !isForm && !isBlob) {
          body = JSON.stringify(body);
          if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/json';
        }
      }

      const res = await fetch(url, { method, headers, body, signal: ctrl ? ctrl.signal : undefined });
      const ct = String(res.headers && res.headers.get ? (res.headers.get('Content-Type') || '') : '');
      const isJsonCt = /application\/json/i.test(ct);

      let data = null;
      if (expectJson || isJsonCt) {
        try { data = await res.json(); } catch (_) { data = null; }
      } else {
        try { data = await res.text(); } catch (_) { data = null; }
      }

      const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
      try { console.debug('[AU.apiFetch]', { method, url, status: res.status, ms: Math.round(t1 - t0) }); } catch(_) {}

      if (!res.ok) {
        const msg = (data && typeof data === 'object' && data.error) ? String(data.error) : `HTTP ${res.status}`;
        const err = new Error(`apiFetch: ${msg}`);
        err.status = res.status;
        err.data = data;
        err.url = url;
        throw err;
      }

      return data;
    } finally {
      if (to) { try { clearTimeout(to); } catch(_) {} }
    }
  };

  // Expose
  try { w.AU = AU; } catch (_) {}
  try { w.nextRaf2 = AU.nextRaf2; } catch (_) {}
})(window);