/**
 * @file
 * Interactive NodeHive API explorer.
 */

(function (Drupal, drupalSettings, once) {
  'use strict';

  var AUTH_STORAGE_KEY = 'nodehive_api_explorer_auth_v1';

  function cloneValue(value) {
    if (value === null || value === undefined) {
      return value;
    }
    return JSON.parse(JSON.stringify(value));
  }

  function isObject(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
  }

  function prettyJson(value) {
    return JSON.stringify(value, null, 2);
  }

  function replacePlaceholdersInString(input, replacements) {
    return String(input).replace(/\{([a-zA-Z0-9_]+)\}/g, function (match, key) {
      if (Object.prototype.hasOwnProperty.call(replacements, key) && replacements[key]) {
        return replacements[key];
      }
      return match;
    });
  }

  function replacePlaceholders(value, replacements) {
    if (typeof value === 'string') {
      return replacePlaceholdersInString(value, replacements);
    }

    if (Array.isArray(value)) {
      return value.map(function (item) {
        return replacePlaceholders(item, replacements);
      });
    }

    if (isObject(value)) {
      var output = {};
      Object.keys(value).forEach(function (key) {
        output[key] = replacePlaceholders(value[key], replacements);
      });
      return output;
    }

    return value;
  }

  function parseJsonObject(raw, label) {
    if (!raw || raw.trim() === '') {
      return { ok: true, value: {} };
    }

    try {
      var parsed = JSON.parse(raw);
      if (!isObject(parsed)) {
        return {
          ok: false,
          error: label + ' must be a JSON object.',
        };
      }
      return { ok: true, value: parsed };
    }
    catch (error) {
      return {
        ok: false,
        error: 'Invalid JSON in ' + label + ': ' + error.message,
      };
    }
  }

  function parseQueryParams(raw) {
    var input = String(raw || '').trim();
    if (input === '') {
      return { ok: true, value: {} };
    }

    // First try JSON object syntax for advanced query structures.
    try {
      var parsedJson = JSON.parse(input);
      if (isObject(parsedJson)) {
        return { ok: true, value: parsedJson };
      }
    }
    catch (error) {
      // Continue with loose query parsing.
    }

    // Also accept classic query-string input: key=value&key2=value2
    // and line-based key:value input.
    var normalized = input.replace(/^\?/, '');
    var segments = normalized.split(/[&\n,]+/).map(function (part) {
      return part.trim();
    }).filter(function (part) {
      return part !== '';
    });

    var value = {};

    function safeDecode(text) {
      try {
        return decodeURIComponent(text);
      }
      catch (e) {
        return text;
      }
    }

    function normalizeLooseKey(key) {
      return String(key || '')
        .trim()
        .replace(/^[\[\(\{'"`]+/, '')
        .replace(/[\]\)\}'"`]+$/, '')
        .trim();
    }

    for (var i = 0; i < segments.length; i++) {
      var segment = segments[i];
      var separatorIndex = segment.indexOf('=');
      var separatorLength = 1;

      // Support key:value lines too.
      if (separatorIndex === -1) {
        separatorIndex = segment.indexOf(':');
      }
      if (separatorIndex === -1) {
        // Bare flags become truthy query params (e.g. "preview" -> preview=1).
        var bareKey = normalizeLooseKey(safeDecode(segment));
        if (bareKey !== '' && bareKey !== '[]') {
          value[bareKey] = '1';
        }
        continue;
      }

      var key = normalizeLooseKey(safeDecode(segment.substring(0, separatorIndex)));
      var decodedValue = safeDecode(segment.substring(separatorIndex + separatorLength));
      if (key === '' || key === '[]') {
        continue;
      }

      if (Object.prototype.hasOwnProperty.call(value, key)) {
        if (!Array.isArray(value[key])) {
          value[key] = [value[key]];
        }
        value[key].push(decodedValue);
      }
      else {
        value[key] = decodedValue;
      }
    }

    return { ok: true, value: value };
  }

  function parseJsonAny(raw, label) {
    if (!raw || raw.trim() === '') {
      return { ok: true, value: null };
    }

    try {
      return { ok: true, value: JSON.parse(raw) };
    }
    catch (error) {
      return {
        ok: false,
        error: 'Invalid JSON in ' + label + ': ' + error.message,
      };
    }
  }

  function parseHeaders(raw) {
    var input = String(raw || '').trim();
    if (input === '') {
      return { ok: true, value: {} };
    }

    // Accept JSON object first.
    try {
      var parsedJson = JSON.parse(input);
      if (isObject(parsedJson)) {
        return { ok: true, value: parsedJson };
      }
    }
    catch (error) {
      // Continue with line parsing.
    }

    // Accept plain header lines:
    // Authorization: Bearer ...
    // X-Foo=bar
    var headers = {};
    function normalizeLooseToken(token) {
      return String(token || '')
        .trim()
        .replace(/^[\[\(\{'"`]+/, '')
        .replace(/[\]\)\}'"`]+$/, '')
        .trim();
    }

    function assignHeader(name, value) {
      var normalizedName = normalizeLooseToken(name);
      var normalizedValue = normalizeLooseToken(value);
      if (normalizedName === '') {
        return;
      }

      var lowerName = normalizedName.toLowerCase();

      // Convenience shorthand for API keys.
      if (['key', 'api_key', 'apikey', 'token', 'bearer'].indexOf(lowerName) !== -1) {
        headers.Authorization = /^bearer\s+/i.test(normalizedValue) ? normalizedValue : ('Bearer ' + normalizedValue);
        return;
      }

      if (lowerName === 'authorization' && /^nhk_/i.test(normalizedValue)) {
        headers.Authorization = 'Bearer ' + normalizedValue;
        return;
      }

      if (lowerName === 'x_api_key') {
        headers['X-API-Key'] = normalizedValue;
        return;
      }

      headers[normalizedName] = normalizedValue;
    }

    var lines = input.split(/\n+/).map(function (line) {
      return line.trim();
    }).filter(function (line) {
      return line !== '';
    });

    for (var i = 0; i < lines.length; i++) {
      var line = lines[i].trim();
      if (line.charAt(0) === '[' && line.charAt(line.length - 1) === ']') {
        line = line.slice(1, -1).trim();
      }

      // If only a raw API key is entered, treat it as Bearer token.
      if (/^nhk_/i.test(line)) {
        headers.Authorization = 'Bearer ' + line;
        continue;
      }

      var separator = line.indexOf(':');
      if (separator === -1) {
        separator = line.indexOf('=');
      }

      if (separator === -1) {
        return {
          ok: false,
          error: 'Headers must be JSON, Header: value lines, or a raw nhk_ API key.',
        };
      }

      var name = line.substring(0, separator).trim();
      var value = line.substring(separator + 1).trim();
      assignHeader(name, value);
    }

    return { ok: true, value: headers };
  }

  function resolveUrl(baseUrl, path) {
    var input = String(path || '').trim();
    if (input === '') {
      throw new Error('Path is required.');
    }

    if (/^https?:\/\//i.test(input)) {
      return input;
    }

    return new URL(input, baseUrl).toString();
  }

  function addQueryParams(url, query) {
    var resolved = new URL(url);

    Object.keys(query).forEach(function (key) {
      var value = query[key];
      if (value === null || value === undefined || value === '') {
        return;
      }

      if (Array.isArray(value)) {
        value.forEach(function (item) {
          if (item !== null && item !== undefined && item !== '') {
            resolved.searchParams.append(key, String(item));
          }
        });
        return;
      }

      if (isObject(value)) {
        resolved.searchParams.set(key, JSON.stringify(value));
        return;
      }

      resolved.searchParams.set(key, String(value));
    });

    return resolved.toString();
  }

  function shellEscapeSingleQuotes(value) {
    return String(value).replace(/'/g, '\'\"\'\"\'');
  }

  function toCurlCommand(request) {
    var parts = ['curl', '-i', '-X', request.method, "'" + shellEscapeSingleQuotes(request.url) + "'"];

    Object.keys(request.headers).forEach(function (name) {
      parts.push('-H');
      parts.push("'" + shellEscapeSingleQuotes(name + ': ' + request.headers[name]) + "'");
    });

    if (request.body !== undefined && request.body !== null && request.body !== '') {
      parts.push('--data');
      parts.push("'" + shellEscapeSingleQuotes(request.body) + "'");
    }

    return parts.join(' ');
  }

  async function copyToClipboard(value) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(value);
      return;
    }

    var textarea = document.createElement('textarea');
    textarea.value = value;
    textarea.setAttribute('readonly', 'readonly');
    textarea.style.position = 'absolute';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
  }

  async function fetchJson(url) {
    try {
      var response = await fetch(url, {
        credentials: 'same-origin',
      });

      var text = await response.text();
      var data = null;
      try {
        data = text ? JSON.parse(text) : null;
      }
      catch (error) {
        data = null;
      }

      return {
        ok: response.ok,
        status: response.status,
        statusText: response.statusText,
        data: data,
        text: text,
      };
    }
    catch (error) {
      return {
        ok: false,
        status: 0,
        statusText: 'Network error',
        error: error.message,
      };
    }
  }

  function getPublicPayload(raw) {
    if (!raw || !isObject(raw)) {
      return null;
    }

    if (raw.status === 'ok' && isObject(raw.data)) {
      return raw.data;
    }

    if (isObject(raw.data)) {
      return raw.data;
    }

    return raw;
  }

  function pickMenuIdFromPublicIndex(payload) {
    if (!payload || !Array.isArray(payload.endpoints)) {
      return null;
    }

    for (var i = 0; i < payload.endpoints.length; i++) {
      var endpoint = payload.endpoints[i];
      if (!endpoint || !Array.isArray(endpoint.available_menus)) {
        continue;
      }
      if (endpoint.available_menus.length > 0 && endpoint.available_menus[0].id) {
        return endpoint.available_menus[0].id;
      }
    }

    return null;
  }

  function chooseDemoTool(tools) {
    if (!Array.isArray(tools) || tools.length === 0) {
      return null;
    }

    for (var i = 0; i < tools.length; i++) {
      if (!tools[i].destructive) {
        return tools[i];
      }
    }

    return tools[0];
  }

  function buildDemoArguments(tool) {
    var args = {};
    if (!tool || !isObject(tool.input)) {
      return args;
    }

    Object.keys(tool.input).forEach(function (name) {
      var definition = tool.input[name] || {};

      if (definition.default !== null && definition.default !== undefined && definition.default !== '') {
        args[name] = definition.default;
      }
      else if (definition.required) {
        args[name] = '<' + (definition.type || 'value') + '>';
      }
    });

    return args;
  }

  function setResponse(pre, payload, isError) {
    if (!pre) {
      return;
    }
    pre.textContent = prettyJson(payload);
    pre.classList.toggle('is-error', !!isError);
  }

  function getCardResponsePre(card) {
    if (!card || !card.element) {
      return null;
    }
    return card.element.querySelector('.nh-api-response');
  }

  function getCardDefaults(state, endpoint) {
    var defaults = {
      path: replacePlaceholdersInString(endpoint.path_template, state.replacements),
      query: replacePlaceholders(cloneValue(endpoint.default_query || {}), state.replacements),
      headers: replacePlaceholders(cloneValue(endpoint.default_headers || {}), state.replacements),
      body: replacePlaceholders(cloneValue(endpoint.default_body || null), state.replacements),
    };

    if (endpoint.id === 'admin-tools-execute' && state.dynamicExecuteBody) {
      defaults.body = replacePlaceholders(cloneValue(state.dynamicExecuteBody), state.replacements);
    }

    return defaults;
  }

  function applyDefaultsToCard(card, defaults) {
    card.pathInput.value = defaults.path;
    card.queryInput.value = prettyJson(defaults.query || {});
    card.headersInput.value = prettyJson(defaults.headers || {});

    if (card.bodyInput) {
      if (defaults.body === null || defaults.body === undefined) {
        card.bodyInput.value = '';
      }
      else {
        card.bodyInput.value = prettyJson(defaults.body);
      }
    }
  }

  function renderMethodBadge(method) {
    var badge = document.createElement('span');
    badge.className = 'nh-api-method nh-api-method--' + method.toLowerCase();
    badge.textContent = method.toUpperCase();
    return badge;
  }

  function createField(labelText, className, type) {
    var field = document.createElement('div');
    field.className = 'nh-api-field ' + className;

    var label = document.createElement('label');
    label.className = 'nh-api-label';
    label.textContent = labelText;

    var input;
    if (type === 'textarea') {
      input = document.createElement('textarea');
      input.className = 'nh-api-textarea';
      input.rows = 5;
      input.spellcheck = false;
    }
    else {
      input = document.createElement('input');
      input.type = 'text';
      input.className = 'nh-api-input';
    }

    field.appendChild(label);
    field.appendChild(input);

    return {
      wrapper: field,
      input: input,
    };
  }

  function renderSummary(summaryContainer, state) {
    summaryContainer.innerHTML = '';

    var summaryTitle = document.createElement('h3');
    summaryTitle.className = 'nh-api-summary__title';
    summaryTitle.textContent = 'Live Configuration Snapshot';
    summaryContainer.appendChild(summaryTitle);

    var list = document.createElement('ul');
    list.className = 'nh-api-summary__list';

    var lines = [];
    lines.push('Base URL: ' + state.settings.baseUrl);

    if (state.live.publicPayload) {
      var payload = state.live.publicPayload;
      var languageCount = Array.isArray(payload.available_languages) ? payload.available_languages.length : 0;
      var spaceCount = Array.isArray(payload.available_spaces) ? payload.available_spaces.length : 0;
      lines.push('Public API: OK (' + languageCount + ' languages, ' + spaceCount + ' spaces)');
    }
    else {
      lines.push('Public API: unavailable for current user/session');
    }

    if (Array.isArray(state.live.tools)) {
      lines.push('Admin tools: OK (' + state.live.tools.length + ' tools visible)');
    }
    else {
      lines.push('Admin tools: unavailable for current user/session');
    }

    if (state.replacements.space_id) {
      lines.push('Demo space_id: ' + state.replacements.space_id);
    }
    if (state.replacements.menu_id) {
      lines.push('Demo menu_id: ' + state.replacements.menu_id);
    }
    if (state.replacements.node_id) {
      lines.push('Demo node_id: ' + state.replacements.node_id);
    }
    if (state.replacements.uuid) {
      lines.push('Demo uuid: ' + state.replacements.uuid);
    }
    if (state.replacements.tool_id) {
      lines.push('Demo tool_id: ' + state.replacements.tool_id);
    }

    state.live.warnings.forEach(function (warning) {
      lines.push('Warning: ' + warning);
    });

    lines.forEach(function (line) {
      var li = document.createElement('li');
      li.textContent = line;
      list.appendChild(li);
    });

    summaryContainer.appendChild(list);
  }

  function createSelectField(labelText, className) {
    var field = document.createElement('div');
    field.className = 'nh-api-field ' + className;

    var label = document.createElement('label');
    label.className = 'nh-api-label';
    label.textContent = labelText;

    var select = document.createElement('select');
    select.className = 'nh-api-select';

    field.appendChild(label);
    field.appendChild(select);

    return {
      wrapper: field,
      input: select,
    };
  }

  function createButton(label, className) {
    var button = document.createElement('button');
    button.type = 'button';
    button.className = className;
    button.textContent = label;
    return button;
  }

  function generateProfileId() {
    return 'auth_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
  }

  function getDefaultAuthState() {
    return {
      mode: 'none',
      selectedProfileId: '',
      selectedProfileSource: '',
      profiles: [],
      discoveredProfiles: [],
      tokenCache: {},
      ui: {},
    };
  }

  function loadStoredAuthState() {
    var fallback = getDefaultAuthState();
    try {
      var raw = localStorage.getItem(AUTH_STORAGE_KEY);
      if (!raw) {
        return fallback;
      }
      var parsed = JSON.parse(raw);
      if (!isObject(parsed)) {
        return fallback;
      }
      return {
        mode: typeof parsed.mode === 'string' ? parsed.mode : 'none',
        selectedProfileId: typeof parsed.selectedProfileId === 'string' ? parsed.selectedProfileId : '',
        selectedProfileSource: '',
        profiles: Array.isArray(parsed.profiles) ? parsed.profiles : [],
        discoveredProfiles: [],
        tokenCache: {},
        ui: {},
      };
    }
    catch (error) {
      return fallback;
    }
  }

  function persistAuthState(state) {
    try {
      localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({
        mode: state.auth.mode,
        selectedProfileId: state.auth.selectedProfileId,
        profiles: state.auth.profiles,
      }));
    }
    catch (error) {
      // Ignore persistence errors (private mode, denied storage, etc).
    }
  }

  function setAuthStatus(state, text, isError) {
    if (!state.auth.ui.status) {
      return;
    }
    state.auth.ui.status.textContent = text || '';
    state.auth.ui.status.classList.toggle('is-error', !!isError);
  }

  function getUserProfilesForMode(state, mode) {
    return state.auth.profiles.filter(function (profile) {
      return profile && profile.type === mode;
    });
  }

  function getDiscoveredProfilesForMode(state, mode) {
    return state.auth.discoveredProfiles.filter(function (profile) {
      return profile && profile.type === mode;
    });
  }

  function getAllProfilesForMode(state, mode) {
    return getUserProfilesForMode(state, mode).concat(getDiscoveredProfilesForMode(state, mode));
  }

  function getUserProfileById(state, profileId) {
    return state.auth.profiles.find(function (profile) {
      return profile && profile.id === profileId;
    }) || null;
  }

  function getAnyProfileById(state, profileId, mode) {
    var profiles = getAllProfilesForMode(state, mode);
    return profiles.find(function (profile) {
      return profile && profile.id === profileId;
    }) || null;
  }

  function setVisible(element, isVisible) {
    element.style.display = isVisible ? '' : 'none';
  }

  function normalizeApiKeyValue(input) {
    var value = String(input || '').trim();
    if (value === '') {
      return '';
    }
    var cleaned = value.replace(/^[\[\(\{'"`]+/, '').replace(/[\]\)\}'"`]+$/, '').trim();
    if (/^bearer\s+/i.test(cleaned)) {
      return cleaned.replace(/^bearer\s+/i, '').trim();
    }
    return cleaned;
  }

  function basicAuthBase64(clientId, clientSecret) {
    var raw = String(clientId || '') + ':' + String(clientSecret || '');
    if (typeof btoa === 'function') {
      return btoa(raw);
    }
    throw new Error('Base64 encoding is not available in this browser.');
  }

  function buildOAuthCacheKey(config) {
    return [config.tokenUrl, config.clientId, config.scope || ''].join('|');
  }

  function authModeLabel(mode) {
    if (mode === 'api_key') {
      return 'API key';
    }
    if (mode === 'oauth_client_credentials') {
      return 'OAuth client_credentials';
    }
    return 'No auth';
  }

  async function fetchOAuthAccessToken(state, config) {
    var cacheKey = buildOAuthCacheKey(config);
    var now = Date.now();
    var cached = state.auth.tokenCache[cacheKey];
    if (cached && cached.accessToken && cached.expiresAt > now + 15000) {
      return cached.accessToken;
    }

    var url = resolveUrl(state.settings.baseUrl, config.tokenUrl || '/oauth/token');
    var params = new URLSearchParams();
    params.set('grant_type', 'client_credentials');
    params.set('client_id', config.clientId);
    params.set('client_secret', config.clientSecret);
    if (config.scope) {
      params.set('scope', config.scope);
    }

    var response = await fetch(url, {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ' + basicAuthBase64(config.clientId, config.clientSecret),
      },
      body: params.toString(),
    });

    var raw = await response.text();
    var data = null;
    try {
      data = raw ? JSON.parse(raw) : null;
    }
    catch (error) {
      data = null;
    }

    if (!response.ok) {
      var message = (data && (data.error_description || data.error)) ? (data.error_description || data.error) : ('HTTP ' + response.status);
      throw new Error('OAuth token request failed: ' + message);
    }

    if (!data || !data.access_token) {
      throw new Error('OAuth token response does not contain access_token.');
    }

    var expiresIn = Number(data.expires_in);
    if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
      expiresIn = 3600;
    }

    state.auth.tokenCache[cacheKey] = {
      accessToken: data.access_token,
      expiresAt: now + (expiresIn * 1000),
    };

    return data.access_token;
  }

  function readAuthConfigFromUi(state) {
    var ui = state.auth.ui;
    var mode = ui.modeSelect ? ui.modeSelect.value : 'none';

    if (mode === 'api_key') {
      return {
        mode: mode,
        profileName: ui.profileNameInput.value.trim(),
        apiKey: normalizeApiKeyValue(ui.apiKeyInput.value),
      };
    }

    if (mode === 'oauth_client_credentials') {
      return {
        mode: mode,
        profileName: ui.profileNameInput.value.trim(),
        tokenUrl: (ui.oauthTokenUrlInput.value || '/oauth/token').trim(),
        clientId: ui.oauthClientIdInput.value.trim(),
        clientSecret: ui.oauthClientSecretInput.value.trim(),
        scope: ui.oauthScopeInput.value.trim(),
      };
    }

    return { mode: 'none' };
  }

  function setAuthConfigToUi(state, mode, profile) {
    var ui = state.auth.ui;
    ui.modeSelect.value = mode || 'none';

    var selectedProfile = profile || null;
    ui.profileNameInput.value = selectedProfile && selectedProfile.name ? selectedProfile.name : '';
    ui.apiKeyInput.value = selectedProfile && selectedProfile.apiKey ? selectedProfile.apiKey : '';
    ui.oauthTokenUrlInput.value = selectedProfile && selectedProfile.tokenUrl ? selectedProfile.tokenUrl : '/oauth/token';
    ui.oauthClientIdInput.value = selectedProfile && selectedProfile.clientId ? selectedProfile.clientId : '';
    ui.oauthClientSecretInput.value = selectedProfile && selectedProfile.clientSecret ? selectedProfile.clientSecret : '';
    ui.oauthScopeInput.value = selectedProfile && selectedProfile.scope ? selectedProfile.scope : '';
  }

  function updateAuthFieldVisibility(state) {
    var ui = state.auth.ui;
    var mode = ui.modeSelect.value;
    setVisible(ui.profileWrap, mode !== 'none');
    setVisible(ui.profileNameWrap, mode !== 'none');
    setVisible(ui.apiKeyWrap, mode === 'api_key');
    setVisible(ui.oauthTokenUrlWrap, mode === 'oauth_client_credentials');
    setVisible(ui.oauthClientIdWrap, mode === 'oauth_client_credentials');
    setVisible(ui.oauthClientSecretWrap, mode === 'oauth_client_credentials');
    setVisible(ui.oauthScopeWrap, mode === 'oauth_client_credentials');
    setVisible(ui.authActions, mode !== 'none');
  }

  function refreshAuthProfileOptions(state) {
    var ui = state.auth.ui;
    var mode = ui.modeSelect.value;
    var userProfiles = getUserProfilesForMode(state, mode);
    var discoveredProfiles = getDiscoveredProfilesForMode(state, mode);
    var allProfiles = userProfiles.concat(discoveredProfiles);

    ui.profileSelect.innerHTML = '';

    var newOption = document.createElement('option');
    newOption.value = '__new__';
    newOption.textContent = 'New profile';
    ui.profileSelect.appendChild(newOption);

    if (userProfiles.length > 0) {
      var userGroup = document.createElement('optgroup');
      userGroup.label = 'Saved profiles';
      userProfiles.forEach(function (profile) {
        var option = document.createElement('option');
        option.value = profile.id;
        option.textContent = profile.name || profile.id;
        userGroup.appendChild(option);
      });
      ui.profileSelect.appendChild(userGroup);
    }

    if (discoveredProfiles.length > 0) {
      var discoveredGroup = document.createElement('optgroup');
      discoveredGroup.label = 'Detected from current config';
      discoveredProfiles.forEach(function (profile) {
        var option = document.createElement('option');
        option.value = profile.id;
        option.textContent = profile.name || profile.id;
        discoveredGroup.appendChild(option);
      });
      ui.profileSelect.appendChild(discoveredGroup);
    }

    if (state.auth.selectedProfileId && allProfiles.some(function (p) { return p.id === state.auth.selectedProfileId; })) {
      ui.profileSelect.value = state.auth.selectedProfileId;
    }
    else {
      ui.profileSelect.value = '__new__';
    }
  }

  function loadSelectedAuthProfileToUi(state) {
    var ui = state.auth.ui;
    var mode = ui.modeSelect.value;
    var profileId = ui.profileSelect.value;
    if (profileId === '__new__') {
      state.auth.selectedProfileId = '';
      state.auth.selectedProfileSource = '';
      setAuthConfigToUi(state, mode, null);
      return;
    }

    var profile = getAnyProfileById(state, profileId, mode);

    state.auth.selectedProfileId = profile ? profile.id : '';
    state.auth.selectedProfileSource = profile && profile._source ? profile._source : '';
    setAuthConfigToUi(state, mode, profile);

    if (profile && profile._note) {
      setAuthStatus(state, profile._note, false);
    }
    else {
      setAuthStatus(state, '', false);
    }
  }

  function saveCurrentAuthProfile(state) {
    var mode = state.auth.ui.modeSelect.value;
    if (mode === 'none') {
      setAuthStatus(state, 'No auth mode selected.', false);
      return;
    }

    var config = readAuthConfigFromUi(state);
    var profileName = config.profileName || (mode === 'api_key' ? 'API key profile' : 'OAuth consumer');
    var profileId = state.auth.ui.profileSelect.value;
    var existingUserProfile = getUserProfileById(state, profileId);
    if (!profileId || profileId === '__new__' || !existingUserProfile) {
      profileId = generateProfileId();
    }

    var profile = {
      id: profileId,
      type: mode,
      name: profileName,
    };

    if (mode === 'api_key') {
      profile.apiKey = config.apiKey;
    }
    if (mode === 'oauth_client_credentials') {
      profile.tokenUrl = config.tokenUrl || '/oauth/token';
      profile.clientId = config.clientId;
      profile.clientSecret = config.clientSecret;
      profile.scope = config.scope;
    }

    var replaced = false;
    state.auth.profiles = state.auth.profiles.map(function (existing) {
      if (existing.id === profileId) {
        replaced = true;
        return profile;
      }
      return existing;
    });
    if (!replaced) {
      state.auth.profiles.push(profile);
    }

    state.auth.mode = mode;
    state.auth.selectedProfileId = profileId;
    state.auth.selectedProfileSource = 'saved';
    persistAuthState(state);
    refreshAuthProfileOptions(state);
    state.auth.ui.profileSelect.value = profileId;
    setAuthStatus(state, 'Saved profile "' + profileName + '".', false);
  }

  function deleteSelectedAuthProfile(state) {
    var profileId = state.auth.ui.profileSelect.value;
    if (!profileId || profileId === '__new__') {
      setAuthStatus(state, 'No saved profile selected.', true);
      return;
    }

    if (!getUserProfileById(state, profileId)) {
      setAuthStatus(state, 'Detected profiles are read-only. Save a copy first if you want to edit.', true);
      return;
    }

    var before = state.auth.profiles.length;
    state.auth.profiles = state.auth.profiles.filter(function (profile) {
      return profile.id !== profileId;
    });

    if (state.auth.profiles.length === before) {
      setAuthStatus(state, 'Profile was not found.', true);
      return;
    }

    state.auth.selectedProfileId = '';
    state.auth.selectedProfileSource = '';
    persistAuthState(state);
    refreshAuthProfileOptions(state);
    loadSelectedAuthProfileToUi(state);
    updateAuthFieldVisibility(state);
    setAuthStatus(state, 'Profile deleted.', false);
  }

  function applyDiscoveredAuthConfig(state, authPayload) {
    if (!authPayload || !isObject(authPayload)) {
      return;
    }

    var discovered = [];

    var apiKeys = Array.isArray(authPayload.api_keys) ? authPayload.api_keys : [];
    apiKeys.forEach(function (item) {
      if (!item || !item.id) {
        return;
      }
      var profileName = item.name || ('API key ' + item.id);
      var keyValue = item.one_time_value || '';
      discovered.push({
        id: 'detected_api_key_' + String(item.id),
        type: 'api_key',
        name: profileName + (item.prefix ? (' (' + item.prefix + '...)') : ''),
        apiKey: keyValue,
        _source: 'detected',
        _note: keyValue
          ? 'Loaded one-time API key value from current configuration.'
          : 'API key value cannot be recovered after creation. Paste it once and save profile if needed.',
      });
    });

    var consumers = Array.isArray(authPayload.oauth_consumers) ? authPayload.oauth_consumers : [];
    consumers.forEach(function (item) {
      if (!item || !item.id || !item.client_id) {
        return;
      }
      if (!item.supports_client_credentials) {
        return;
      }

      discovered.push({
        id: 'detected_oauth_consumer_' + String(item.id),
        type: 'oauth_client_credentials',
        name: item.label || item.client_id,
        tokenUrl: item.token_url || '/oauth/token',
        clientId: item.client_id,
        clientSecret: '',
        scope: '',
        _source: 'detected',
        _note: 'Loaded consumer from current configuration. Enter client_secret once, then save a local profile.',
      });
    });

    state.auth.discoveredProfiles = discovered;
    if (state.auth.ui && state.auth.ui.modeSelect) {
      refreshAuthProfileOptions(state);
      if (state.auth.selectedProfileId) {
        state.auth.ui.profileSelect.value = state.auth.selectedProfileId;
      }
      loadSelectedAuthProfileToUi(state);
      updateAuthFieldVisibility(state);
    }
  }

  function autoSelectSavedAuthProfile(state) {
    var ui = state.auth.ui;
    if (!ui || !ui.modeSelect || !ui.profileSelect) {
      return;
    }

    if (ui.modeSelect.value !== 'none') {
      return;
    }

    var savedApiProfiles = getUserProfilesForMode(state, 'api_key');

    // If exactly one saved API key profile exists, auto-select it.
    if (savedApiProfiles.length === 1) {
      ui.modeSelect.value = 'api_key';
      state.auth.mode = 'api_key';
      state.auth.selectedProfileId = savedApiProfiles[0].id;
      state.auth.selectedProfileSource = 'saved';
      refreshAuthProfileOptions(state);
      ui.profileSelect.value = savedApiProfiles[0].id;
      loadSelectedAuthProfileToUi(state);
      updateAuthFieldVisibility(state);
      persistAuthState(state);
      setAuthStatus(state, 'Auto-selected saved API key profile "' + (savedApiProfiles[0].name || savedApiProfiles[0].id) + '".', false);
    }
  }

  async function buildAuthHeaders(state) {
    var config = readAuthConfigFromUi(state);

    if (config.mode === 'none') {
      return {
        headers: {},
        label: 'No auth',
      };
    }

    if (config.mode === 'api_key') {
      if (!config.apiKey) {
        throw new Error('API key is empty.');
      }
      return {
        headers: {
          Authorization: 'Bearer ' + config.apiKey,
        },
        label: config.profileName ? ('API key (' + config.profileName + ')') : 'API key',
      };
    }

    if (config.mode === 'oauth_client_credentials') {
      if (!config.clientId || !config.clientSecret) {
        throw new Error('OAuth client_id and client_secret are required.');
      }
      var accessToken = await fetchOAuthAccessToken(state, config);
      return {
        headers: {
          Authorization: 'Bearer ' + accessToken,
        },
        label: config.profileName ? ('OAuth (' + config.profileName + ')') : 'OAuth client_credentials',
      };
    }

    return {
      headers: {},
      label: authModeLabel(config.mode),
    };
  }

  function getActiveCard(state) {
    if (!state || !state.activeEndpointId || !Array.isArray(state.cards)) {
      return null;
    }
    return state.cards.find(function (card) {
      return card.endpoint && card.endpoint.id === state.activeEndpointId;
    }) || null;
  }

  async function applyAuthToActiveCard(state) {
    var card = getActiveCard(state);
    if (!card) {
      setAuthStatus(state, 'No endpoint selected.', true);
      return;
    }

    var parsedHeaders = parseHeaders(card.headersInput.value);
    var authResult = await buildAuthHeaders(state);
    var mergedHeaders = {};

    if (parsedHeaders.ok) {
      Object.keys(parsedHeaders.value).forEach(function (name) {
        mergedHeaders[name] = String(parsedHeaders.value[name]);
      });
    }
    Object.keys(authResult.headers).forEach(function (name) {
      mergedHeaders[name] = String(authResult.headers[name]);
    });

    card.headersInput.value = prettyJson(mergedHeaders);
    card.dirty = true;
    if (parsedHeaders.ok) {
      setAuthStatus(state, 'Auth header set on selected endpoint (' + (authResult.label || 'auth') + ').', false);
    }
    else {
      setAuthStatus(state, 'Headers were invalid and have been replaced with clean auth headers.', false);
    }
  }

  function renderAuthPanel(sidebar, state) {
    var authPanel = document.createElement('section');
    authPanel.className = 'nh-api-auth';
    authPanel.id = 'nh-api-auth-panel';

    var title = document.createElement('h3');
    title.className = 'nh-api-auth__title';
    title.textContent = 'Authentication';
    authPanel.appendChild(title);

    var modeField = createSelectField('Auth method', 'nh-api-auth__mode');
    [
      { value: 'none', label: 'No auth' },
      { value: 'api_key', label: 'API key (Bearer)' },
      { value: 'oauth_client_credentials', label: 'OAuth consumer (client_credentials)' },
    ].forEach(function (item) {
      var option = document.createElement('option');
      option.value = item.value;
      option.textContent = item.label;
      modeField.input.appendChild(option);
    });
    modeField.input.name = 'auth_mode';
    authPanel.appendChild(modeField.wrapper);

    var profileField = createSelectField('Profile', 'nh-api-auth__profile');
    profileField.input.name = 'auth_profile_id';
    authPanel.appendChild(profileField.wrapper);

    var profileNameField = createField('Profile name', 'nh-api-auth__profile-name', 'input');
    profileNameField.input.name = 'auth_profile_name';
    authPanel.appendChild(profileNameField.wrapper);

    var apiKeyField = createField('API key', 'nh-api-auth__api-key', 'input');
    apiKeyField.input.name = 'auth_api_key';
    authPanel.appendChild(apiKeyField.wrapper);

    var oauthTokenUrlField = createField('Token URL', 'nh-api-auth__oauth-token-url', 'input');
    oauthTokenUrlField.input.name = 'auth_token_url';
    oauthTokenUrlField.input.placeholder = '/oauth/token';
    authPanel.appendChild(oauthTokenUrlField.wrapper);

    var oauthClientIdField = createField('OAuth client_id', 'nh-api-auth__oauth-client-id', 'input');
    oauthClientIdField.input.name = 'auth_client_id';
    authPanel.appendChild(oauthClientIdField.wrapper);

    var oauthClientSecretField = createField('OAuth client_secret', 'nh-api-auth__oauth-client-secret', 'input');
    oauthClientSecretField.input.name = 'auth_client_secret';
    oauthClientSecretField.input.type = 'password';
    oauthClientSecretField.input.autocomplete = 'off';
    authPanel.appendChild(oauthClientSecretField.wrapper);

    var oauthScopeField = createField('OAuth scope (optional)', 'nh-api-auth__oauth-scope', 'input');
    oauthScopeField.input.name = 'auth_scope';
    authPanel.appendChild(oauthScopeField.wrapper);

    var actions = document.createElement('div');
    actions.className = 'nh-api-auth__actions';
    var saveButton = createButton('Save profile', 'button button--small');
    var deleteButton = createButton('Delete profile', 'button button--small');
    var setAuthButton = createButton('Set auth', 'button button--small button--primary');
    actions.appendChild(setAuthButton);
    actions.appendChild(saveButton);
    actions.appendChild(deleteButton);
    authPanel.appendChild(actions);

    var status = document.createElement('p');
    status.className = 'nh-api-auth__status';
    authPanel.appendChild(status);

    state.auth.ui = {
      modeSelect: modeField.input,
      profileSelect: profileField.input,
      profileWrap: profileField.wrapper,
      profileNameInput: profileNameField.input,
      profileNameWrap: profileNameField.wrapper,
      apiKeyInput: apiKeyField.input,
      apiKeyWrap: apiKeyField.wrapper,
      oauthTokenUrlInput: oauthTokenUrlField.input,
      oauthTokenUrlWrap: oauthTokenUrlField.wrapper,
      oauthClientIdInput: oauthClientIdField.input,
      oauthClientIdWrap: oauthClientIdField.wrapper,
      oauthClientSecretInput: oauthClientSecretField.input,
      oauthClientSecretWrap: oauthClientSecretField.wrapper,
      oauthScopeInput: oauthScopeField.input,
      oauthScopeWrap: oauthScopeField.wrapper,
      authActions: actions,
      status: status,
    };

    state.auth.ui.modeSelect.value = state.auth.mode || 'none';
    refreshAuthProfileOptions(state);
    loadSelectedAuthProfileToUi(state);
    updateAuthFieldVisibility(state);
    autoSelectSavedAuthProfile(state);

    modeField.input.addEventListener('change', function () {
      state.auth.mode = modeField.input.value;
      state.auth.selectedProfileId = '';
      refreshAuthProfileOptions(state);
      loadSelectedAuthProfileToUi(state);
      updateAuthFieldVisibility(state);
      persistAuthState(state);
      setAuthStatus(state, '', false);
    });

    profileField.input.addEventListener('change', function () {
      loadSelectedAuthProfileToUi(state);
      updateAuthFieldVisibility(state);
      persistAuthState(state);
      setAuthStatus(state, '', false);
    });

    saveButton.addEventListener('click', function () {
      saveCurrentAuthProfile(state);
    });

    deleteButton.addEventListener('click', function () {
      deleteSelectedAuthProfile(state);
    });

    setAuthButton.addEventListener('click', async function () {
      try {
        await applyAuthToActiveCard(state);
      }
      catch (error) {
        setAuthStatus(state, error.message, true);
      }
    });

    sidebar.appendChild(authPanel);
  }

  async function runRequest(card, state) {
    var queryParsed = parseQueryParams(card.queryInput.value);
    if (!queryParsed.ok) {
      setResponse(getCardResponsePre(card), { error: queryParsed.error }, true);
      return;
    }

    var headersParsed = parseHeaders(card.headersInput.value);
    if (!headersParsed.ok) {
      setResponse(getCardResponsePre(card), { error: headersParsed.error }, true);
      return;
    }

    var body = null;
    if (card.bodyInput) {
      var bodyParsed = parseJsonAny(card.bodyInput.value, 'Body');
      if (!bodyParsed.ok) {
        setResponse(getCardResponsePre(card), { error: bodyParsed.error }, true);
        return;
      }
      body = bodyParsed.value;
    }

    var request;
    try {
      var url = resolveUrl(state.settings.baseUrl, card.pathInput.value);
      url = addQueryParams(url, queryParsed.value);

      var headers = {};
      Object.keys(headersParsed.value).forEach(function (name) {
        headers[name] = String(headersParsed.value[name]);
      });
      var authResult = await buildAuthHeaders(state);
      Object.keys(authResult.headers).forEach(function (name) {
        headers[name] = authResult.headers[name];
      });
      setAuthStatus(state, authResult.label + ' applied.', false);

      var options = {
        method: card.endpoint.method.toUpperCase(),
        headers: headers,
        credentials: 'same-origin',
      };

      var bodyText = null;
      if (options.method !== 'GET' && options.method !== 'HEAD' && body !== null) {
        bodyText = JSON.stringify(body);
        options.body = bodyText;

        if (!Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) {
          options.headers['Content-Type'] = 'application/json';
          headers['Content-Type'] = 'application/json';
        }
      }

      request = {
        method: options.method,
        url: url,
        headers: headers,
        body: bodyText,
        auth: authResult.label,
      };

      setResponse(getCardResponsePre(card), {
        request: request,
        response: {
          status: 'Loading...',
        },
      }, false);

      var startedAt = Date.now();
      var response = await fetch(url, options);
      var durationMs = Date.now() - startedAt;

      var raw = await response.text();
      var parsedData = null;
      try {
        parsedData = raw ? JSON.parse(raw) : null;
      }
      catch (error) {
        parsedData = raw;
      }

      setResponse(getCardResponsePre(card), {
        request: request,
        response: {
          status: response.status,
          statusText: response.statusText,
          duration_ms: durationMs,
        },
        data: parsedData,
      }, !response.ok);
    }
    catch (error) {
      setAuthStatus(state, error.message, true);
      setResponse(getCardResponsePre(card), {
        request: request || null,
        error: error.message,
      }, true);
    }
  }

  function refreshCardsFromState(state) {
    state.cards.forEach(function (card) {
      if (card.dirty) {
        return;
      }

      var defaults = getCardDefaults(state, card.endpoint);
      applyDefaultsToCard(card, defaults);
    });
  }

  async function loadLiveConfiguration(state, summaryContainer) {
    var liveEndpoints = state.settings.liveEndpoints || {};

    if (liveEndpoints.publicIndex) {
      var publicResult = await fetchJson(liveEndpoints.publicIndex);
      if (publicResult.ok && publicResult.data) {
        var publicPayload = getPublicPayload(publicResult.data);
        state.live.publicPayload = publicPayload;

        if (publicPayload) {
          var firstSpace = Array.isArray(publicPayload.available_spaces) && publicPayload.available_spaces.length > 0
            ? publicPayload.available_spaces[0]
            : null;
          if (firstSpace && firstSpace.id) {
            state.replacements.space_id = String(firstSpace.id);
          }

          var firstMenuId = pickMenuIdFromPublicIndex(publicPayload);
          if (firstMenuId) {
            state.replacements.menu_id = String(firstMenuId);
          }

          if (state.replacements.space_id && liveEndpoints.publicSpaceNodesTemplate) {
            var nodeListUrl = liveEndpoints.publicSpaceNodesTemplate.replace('{space_id}', encodeURIComponent(state.replacements.space_id));
            var nodeListResult = await fetchJson(nodeListUrl);
            if (nodeListResult.ok && nodeListResult.data) {
              var nodePayload = getPublicPayload(nodeListResult.data);
              if (nodePayload && Array.isArray(nodePayload.items) && nodePayload.items.length > 0) {
                if (nodePayload.items[0].id) {
                  state.replacements.node_id = String(nodePayload.items[0].id);
                }
                if (nodePayload.items[0].uuid) {
                  state.replacements.uuid = String(nodePayload.items[0].uuid);
                }
              }
            }
          }
        }
      }
      else {
        state.live.warnings.push('Public API bootstrap failed (' + (publicResult.status || 'network') + ').');
      }
    }

    if (liveEndpoints.adminTools) {
      var toolsResult = await fetchJson(liveEndpoints.adminTools);
      if (toolsResult.ok && toolsResult.data && Array.isArray(toolsResult.data.tools)) {
        state.live.tools = toolsResult.data.tools;

        var demoTool = chooseDemoTool(state.live.tools);
        if (demoTool && demoTool.id) {
          state.replacements.tool_id = String(demoTool.id);
          state.dynamicExecuteBody = {
            tool_id: demoTool.id,
            arguments: buildDemoArguments(demoTool),
          };
        }
      }
      else {
        state.live.warnings.push('Admin tools bootstrap failed (' + (toolsResult.status || 'network') + ').');
      }
    }

    if (liveEndpoints.authConfig) {
      var authConfigResult = await fetchJson(liveEndpoints.authConfig);
      if (authConfigResult.ok && authConfigResult.data) {
        var authPayload = getPublicPayload(authConfigResult.data);
        applyDiscoveredAuthConfig(state, authPayload);
        if (authPayload && Array.isArray(authPayload.warnings)) {
          authPayload.warnings.forEach(function (warning) {
            if (warning) {
              state.live.warnings.push('Auth config: ' + warning);
            }
          });
        }
      }
      else {
        state.live.warnings.push('Auth config bootstrap failed (' + (authConfigResult.status || 'network') + ').');
      }
    }

    refreshCardsFromState(state);
    renderSummary(summaryContainer, state);
  }

  function setActiveEndpoint(state, endpointId) {
    state.activeEndpointId = endpointId;

    state.cards.forEach(function (card) {
      var isActive = card.endpoint.id === endpointId;
      card.element.classList.toggle('is-active', isActive);
      card.sidebarButton.classList.toggle('is-active', isActive);
      card.sidebarButton.setAttribute('aria-current', isActive ? 'true' : 'false');
    });
  }

  function renderExplorer(container, settings) {
    var state = {
      settings: settings,
      replacements: {
        menu_id: 'main',
        space_id: '1',
        node_id: '1',
        uuid: '00000000-0000-0000-0000-000000000000',
        tool_id: 'display_message',
      },
      dynamicExecuteBody: null,
      live: {
        publicPayload: null,
        tools: null,
        warnings: [],
      },
      cards: [],
      activeEndpointId: null,
      auth: loadStoredAuthState(),
    };
    var executeUrl = (settings.liveEndpoints && settings.liveEndpoints.execute)
      ? settings.liveEndpoints.execute
      : '/nodehive/api-explorer/execute';

    container.innerHTML = '';

    var intro = document.createElement('div');
    intro.className = 'nh-api-explorer__intro';
    intro.innerHTML =
      '<h2>NodeHive API Explorer</h2>' +
      '<p>Inspect both APIs and run real requests against this environment.</p>';
    container.appendChild(intro);

    var summary = document.createElement('section');
    summary.className = 'nh-api-summary';
    container.appendChild(summary);

    var workspace = document.createElement('div');
    workspace.className = 'nh-api-workspace';
    container.appendChild(workspace);

    var sidebar = document.createElement('aside');
    sidebar.className = 'nh-api-sidebar';
    workspace.appendChild(sidebar);

    renderAuthPanel(sidebar, state);

    var sidebarTitle = document.createElement('h3');
    sidebarTitle.className = 'nh-api-sidebar__title';
    sidebarTitle.textContent = 'Endpoints';
    sidebar.appendChild(sidebarTitle);

    var detail = document.createElement('section');
    detail.className = 'nh-api-detail';
    workspace.appendChild(detail);

    (settings.endpointGroups || []).forEach(function (group) {
      var sidebarGroup = document.createElement('div');
      sidebarGroup.className = 'nh-api-sidebar__group';

      var groupTitle = document.createElement('h4');
      groupTitle.className = 'nh-api-sidebar__group-title';
      groupTitle.textContent = group.title;
      sidebarGroup.appendChild(groupTitle);

      var groupDescription = document.createElement('p');
      groupDescription.className = 'nh-api-sidebar__group-description';
      groupDescription.textContent = group.description || '';
      sidebarGroup.appendChild(groupDescription);

      (group.endpoints || []).forEach(function (endpoint) {
        var endpointButton = document.createElement('button');
        endpointButton.type = 'button';
        endpointButton.className = 'nh-api-endpoint-btn';

        var endpointButtonTop = document.createElement('span');
        endpointButtonTop.className = 'nh-api-endpoint-btn__top';
        endpointButtonTop.appendChild(renderMethodBadge(endpoint.method));

        var endpointLabel = document.createElement('span');
        endpointLabel.className = 'nh-api-endpoint-btn__label';
        endpointLabel.textContent = endpoint.label;
        endpointButtonTop.appendChild(endpointLabel);
        endpointButton.appendChild(endpointButtonTop);

        var endpointPath = document.createElement('span');
        endpointPath.className = 'nh-api-endpoint-btn__path';
        endpointPath.textContent = endpoint.path_template;
        endpointButton.appendChild(endpointPath);

        sidebarGroup.appendChild(endpointButton);

        var card = document.createElement('article');
        card.className = 'nh-api-card nh-api-panel';

        var top = document.createElement('div');
        top.className = 'nh-api-card__top';

        top.appendChild(renderMethodBadge(endpoint.method));

        var panelLabel = document.createElement('h4');
        panelLabel.className = 'nh-api-card__title';
        panelLabel.textContent = endpoint.label;
        top.appendChild(panelLabel);

        card.appendChild(top);

        var endpointDescription = document.createElement('p');
        endpointDescription.className = 'nh-api-card__description';
        endpointDescription.textContent = endpoint.description || '';
        card.appendChild(endpointDescription);

        var requestWrap = document.createElement('form');
        requestWrap.className = 'nh-api-card__request';
        requestWrap.setAttribute('hx-post', executeUrl);
        requestWrap.setAttribute('hx-swap', 'innerHTML');
        requestWrap.setAttribute('hx-include', '#nh-api-auth-panel [name]');
        requestWrap.setAttribute('autocomplete', 'off');
        var requestTitle = document.createElement('h5');
        requestTitle.className = 'nh-api-section-title';
        requestTitle.textContent = 'Request Builder';
        requestWrap.appendChild(requestTitle);

        var pathField = createField('Path', 'nh-api-field--path', 'input');
        pathField.input.name = 'path';
        requestWrap.appendChild(pathField.wrapper);

        var queryField = createField('Query params (JSON or key=value)', 'nh-api-field--query', 'textarea');
        queryField.input.name = 'query';
        queryField.input.rows = 3;
        requestWrap.appendChild(queryField.wrapper);

        var headersField = createField('Headers (JSON or Header: value)', 'nh-api-field--headers', 'textarea');
        headersField.input.name = 'headers';
        headersField.input.rows = 3;
        requestWrap.appendChild(headersField.wrapper);

        var bodyField = null;
        if (endpoint.method.toUpperCase() !== 'GET' && endpoint.method.toUpperCase() !== 'HEAD') {
          bodyField = createField('Request body (JSON)', 'nh-api-field--body', 'textarea');
          bodyField.input.name = 'body';
          bodyField.input.rows = 7;
          requestWrap.appendChild(bodyField.wrapper);
        }

        var methodInput = document.createElement('input');
        methodInput.type = 'hidden';
        methodInput.name = 'method';
        methodInput.value = endpoint.method.toUpperCase();
        requestWrap.appendChild(methodInput);

        var actions = document.createElement('div');
        actions.className = 'nh-api-card__actions';

        var sendButton = document.createElement('button');
        sendButton.type = 'submit';
        sendButton.className = 'button button--primary nh-api-btn';
        sendButton.textContent = 'Run request';
        actions.appendChild(sendButton);

        var curlButton = document.createElement('button');
        curlButton.type = 'button';
        curlButton.className = 'button nh-api-btn';
        curlButton.textContent = 'Copy cURL';
        actions.appendChild(curlButton);

        var resetButton = document.createElement('button');
        resetButton.type = 'button';
        resetButton.className = 'button nh-api-btn';
        resetButton.textContent = 'Reset';
        actions.appendChild(resetButton);

        requestWrap.appendChild(actions);
        card.appendChild(requestWrap);

        var responseWrap = document.createElement('div');
        responseWrap.className = 'nh-api-card__response';
        var responseTitle = document.createElement('h5');
        responseTitle.className = 'nh-api-section-title nh-api-section-title--response';
        responseTitle.textContent = 'Response';
        responseWrap.appendChild(responseTitle);
        var responseShell = document.createElement('div');
        responseShell.className = 'nh-api-response-shell';
        var responseShellId = 'nh-api-response-shell-' + endpoint.id;
        responseShell.id = responseShellId;
        requestWrap.setAttribute('hx-target', '#' + responseShellId);
        var responsePre = document.createElement('pre');
        responsePre.className = 'nh-api-response';
        responsePre.textContent = prettyJson({ info: 'Run a request to view response.' });
        responseShell.appendChild(responsePre);
        responseWrap.appendChild(responseShell);
        card.appendChild(responseWrap);

        var cardRef = {
          endpoint: endpoint,
          element: card,
          sidebarButton: endpointButton,
          pathInput: pathField.input,
          queryInput: queryField.input,
          headersInput: headersField.input,
          bodyInput: bodyField ? bodyField.input : null,
          responsePre: responsePre,
          responseShell: responseShell,
          dirty: false,
        };

        [cardRef.pathInput, cardRef.queryInput, cardRef.headersInput, cardRef.bodyInput].forEach(function (input) {
          if (!input) {
            return;
          }
          input.addEventListener('input', function () {
            cardRef.dirty = true;
          });
        });

        requestWrap.addEventListener('htmx:beforeRequest', function () {
          cardRef.responseShell.innerHTML = '<pre class="nh-api-response">{\n  "info": "Loading..."\n}</pre>';
          cardRef.responsePre = getCardResponsePre(cardRef);
        });

        requestWrap.addEventListener('htmx:afterSwap', function (event) {
          if (event.target === cardRef.responseShell) {
            cardRef.responsePre = getCardResponsePre(cardRef);
          }
        });

        curlButton.addEventListener('click', async function () {
          var queryParsed = parseQueryParams(cardRef.queryInput.value);
          var headersParsed = parseHeaders(cardRef.headersInput.value);
          var bodyParsed = cardRef.bodyInput ? parseJsonAny(cardRef.bodyInput.value, 'Body') : { ok: true, value: null };

          if (!queryParsed.ok || !headersParsed.ok || !bodyParsed.ok) {
            setResponse(getCardResponsePre(cardRef), {
              error: queryParsed.error || headersParsed.error || bodyParsed.error,
            }, true);
            return;
          }

          try {
            var url = addQueryParams(resolveUrl(state.settings.baseUrl, cardRef.pathInput.value), queryParsed.value);
            var headers = {};
            Object.keys(headersParsed.value).forEach(function (name) {
              headers[name] = String(headersParsed.value[name]);
            });
            var authResult = await buildAuthHeaders(state);
            Object.keys(authResult.headers).forEach(function (name) {
              headers[name] = authResult.headers[name];
            });
            setAuthStatus(state, authResult.label + ' applied.', false);

            var bodyText = null;
            if (cardRef.endpoint.method.toUpperCase() !== 'GET' && cardRef.endpoint.method.toUpperCase() !== 'HEAD' && bodyParsed.value !== null) {
              bodyText = JSON.stringify(bodyParsed.value);
              if (!Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) {
                headers['Content-Type'] = 'application/json';
              }
            }

            var curl = toCurlCommand({
              method: cardRef.endpoint.method.toUpperCase(),
              url: url,
              headers: headers,
              body: bodyText,
            });
            await copyToClipboard(curl);
            setResponse(getCardResponsePre(cardRef), {
              info: 'cURL copied to clipboard.',
              curl: curl,
            }, false);
          }
          catch (error) {
            setAuthStatus(state, error.message, true);
            setResponse(getCardResponsePre(cardRef), {
              error: error.message,
            }, true);
          }
        });

        resetButton.addEventListener('click', function () {
          cardRef.dirty = false;
          applyDefaultsToCard(cardRef, getCardDefaults(state, endpoint));
          setResponse(getCardResponsePre(cardRef), { info: 'Request reset to defaults.' }, false);
        });

        endpointButton.addEventListener('click', function () {
          setActiveEndpoint(state, endpoint.id);
        });

        applyDefaultsToCard(cardRef, getCardDefaults(state, endpoint));
        state.cards.push(cardRef);
        detail.appendChild(card);
      });

      sidebar.appendChild(sidebarGroup);
    });

    if (state.cards.length > 0) {
      setActiveEndpoint(state, state.cards[0].endpoint.id);
    }

    renderSummary(summary, state);
    loadLiveConfiguration(state, summary);
  }

  Drupal.behaviors.nodehiveApiExplorer = {
    attach: function (context) {
      once('nodehive-api-explorer', '#nodehive-api-explorer', context).forEach(function (container) {
        var settings = drupalSettings.nodehiveApiExplorer;
        if (!settings) {
          container.textContent = 'Missing API explorer settings.';
          return;
        }

        renderExplorer(container, settings);
      });
    },
  };

})(Drupal, drupalSettings, once);
