(function () {
  if (window.__appWs) return;

  var RECONNECT_BASE = 1000;
  var RECONNECT_MAX = 8000;
  var HEARTBEAT_MS = 25000;
  var REQUEST_TIMEOUT = 30000;

  var ws = null;
  var connected = false;
  var reconnectDelay = RECONNECT_BASE;
  var reconnectTimer = null;
  var heartbeatTimer = null;
  var pendingRequests = {};
  var requestCounter = 0;
  var intentionalClose = false;
  var originalFetch = window.fetch;

  // Chat pub/sub state — bot:* / chat:* events forwarded from the supervisor's chat
  // broadcaster. Listeners are independent of WS lifecycle: when the WS reconnects we
  // re-send the subscribe message automatically.
  var chatListeners = [];
  var chatSubscribed = false;
  var chatClientId = null;

  function emitChatEvent(eventType, data) {
    for (var i = 0; i < chatListeners.length; i++) {
      try { chatListeners[i](eventType, data); } catch (err) { console.error('[app-ws] chat listener error', err); }
    }
  }

  function sendChatSubscribe() {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    ws.send(JSON.stringify({ type: 'chat:subscribe', data: { clientId: chatClientId } }));
  }

  function buildWsUrl() {
    var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    return proto + '//' + location.host + '/app/ws';
  }

  function startHeartbeat() {
    stopHeartbeat();
    heartbeatTimer = setInterval(function () {
      if (ws && ws.readyState === WebSocket.OPEN) ws.send('ping');
    }, HEARTBEAT_MS);
  }

  function stopHeartbeat() {
    if (heartbeatTimer) {
      clearInterval(heartbeatTimer);
      heartbeatTimer = null;
    }
  }

  function failoverPending() {
    var ids = Object.keys(pendingRequests);
    for (var i = 0; i < ids.length; i++) {
      var p = pendingRequests[ids[i]];
      clearTimeout(p.timer);
      p.resolve(originalFetch(p.input, p.init));
    }
    pendingRequests = {};
  }

  function connect() {
    intentionalClose = false;
    var url = buildWsUrl();
    ws = new WebSocket(url);

    ws.onopen = function () {
      connected = true;
      reconnectDelay = RECONNECT_BASE;
      startHeartbeat();
      if (chatSubscribed) sendChatSubscribe();
    };

    ws.onmessage = function (e) {
      if (e.data === 'pong') return;

      var msg;
      try {
        msg = JSON.parse(e.data);
      } catch (err) {
        return;
      }

      if (msg.type === 'chat:event' && msg.data) {
        emitChatEvent(msg.data.eventType, msg.data.eventData);
        return;
      }

      if (msg.type === 'chat:subscribed') {
        return;
      }

      if (msg.type === 'app:api:response' && msg.data && msg.data.id) {
        var pending = pendingRequests[msg.data.id];
        if (pending) {
          clearTimeout(pending.timer);
          delete pendingRequests[msg.data.id];


          var responseBody = msg.data.body;
          if (responseBody === null || responseBody === undefined) responseBody = '';

          var responseInit = {
            status: msg.data.status,
            headers: msg.data.headers || {},
          };

          pending.resolve(new Response(responseBody, responseInit));
        }
      }
    };

    ws.onclose = function () {
      connected = false;
      stopHeartbeat();
      failoverPending();

      if (!intentionalClose) {
        reconnectTimer = setTimeout(function () {
          reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
          connect();
        }, reconnectDelay);
      }
    };

    ws.onerror = function () {
      if (ws) ws.close();
    };
  }

  // Override window.fetch to intercept /app/api/* calls
  window.fetch = function (input, init) {
    var url = input instanceof Request ? input.url : String(input);
    var method = (init && init.method) || (input instanceof Request ? input.method : 'GET');

    // Only intercept /app/api/* calls
    if (url.indexOf('/app/api') === -1) {
      return originalFetch.apply(this, arguments);
    }

    // Fallback: WS not connected
    if (!connected || !ws || ws.readyState !== WebSocket.OPEN) {
      return originalFetch.apply(this, arguments);
    }

    // Fallback: non-serializable body (FormData, Blob, ArrayBuffer)
    var body = init && init.body;
    if (body && (body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer)) {
      return originalFetch.apply(this, arguments);
    }

    // Build request
    var id = 'r' + ++requestCounter + '-' + Date.now().toString(36);

    var reqPath = url;
    try {
      var parsed = new URL(url, location.origin);
      reqPath = parsed.pathname + parsed.search;
    } catch (e) {}

    var headers = {};
    if (init && init.headers) {
      if (init.headers instanceof Headers) {
        init.headers.forEach(function (v, k) {
          headers[k] = v;
        });
      } else if (typeof init.headers === 'object') {
        var h = init.headers;
        for (var k in h) {
          if (Object.prototype.hasOwnProperty.call(h, k)) headers[k] = h[k];
        }
      }
    }

    var bodyStr = null;
    if (body !== undefined && body !== null) {
      bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
    }


    ws.send(
      JSON.stringify({
        type: 'app:api',
        data: { id: id, method: method.toUpperCase(), path: reqPath, headers: headers, body: bodyStr },
      })
    );

    var savedInput = input;
    var savedInit = init;

    return new Promise(function (resolve, reject) {
      var timer = setTimeout(function () {
        delete pendingRequests[id];
        resolve(originalFetch(savedInput, savedInit));
      }, REQUEST_TIMEOUT);

      pendingRequests[id] = {
        resolve: resolve,
        reject: reject,
        timer: timer,
        input: savedInput,
        init: savedInit,
        method: method,
        path: reqPath,
      };
    });
  };

  window.__appWs = {
    connected: function () {
      return connected;
    },
    // Subscribe to chat events (bot:typing, bot:token, bot:response, chat:sync, …).
    // Returns an unsubscribe function. Multiple listeners are supported. The underlying
    // subscribe message is sent on first listener registration and re-sent on reconnect.
    onChatEvent: function (handler) {
      chatListeners.push(handler);
      if (!chatSubscribed) {
        chatSubscribed = true;
        chatClientId =
          (typeof crypto !== 'undefined' && crypto.randomUUID)
            ? crypto.randomUUID()
            : 'ws-' + Math.random().toString(36).slice(2);
        if (ws && ws.readyState === WebSocket.OPEN) sendChatSubscribe();
      }
      return function unsubscribe() {
        var i = chatListeners.indexOf(handler);
        if (i >= 0) chatListeners.splice(i, 1);
      };
    },
    // Stable per-tab clientId used to identify this subscriber to the server. Useful
    // when calling /api/agent/chat/message so the server can skip echoing chat:sync back.
    clientId: function () {
      return chatClientId;
    },
  };

  connect();
})();
