/**
 * app-bookmark-save.js
 * Bookmark dialog (create/update) + group dialog (create/update).
 */

import { showAsyncFlash } from '/js/components/app-async-flash.js';
import { warnOnce, resolveHost } from '/js/core/app-dom.js';
import { postForm } from '/js/core/app-http.js';
import { getAppBase } from '/js/pages/app-list-utils.js';

export function initBookmarkSave(root = document, options = {}) {
  const host = resolveHost(root);
  const openButtonSelector = String(options.openButtonSelector || '[data-bookmark-open-dialog]').trim() || '[data-bookmark-open-dialog]';
  const bookmarkDialogSelector = String(options.dialogSelector || '[data-app-bookmark-dialog]').trim() || '[data-app-bookmark-dialog]';
  const groupDialogSelector = String(options.groupDialogSelector || '[data-app-bookmark-group-dialog]').trim() || '[data-app-bookmark-group-dialog]';
  const openBtn = host.querySelector(openButtonSelector);
  const bookmarkDialog = host.matches?.(bookmarkDialogSelector)
    ? host
    : host.querySelector(bookmarkDialogSelector);
  if (!(bookmarkDialog instanceof HTMLDialogElement)) {
    return { destroy: () => {} };
  }
  if (bookmarkDialog.dataset.bookmarkSaveBound === '1' && bookmarkDialog._bookmarkSaveApi) {
    return bookmarkDialog._bookmarkSaveApi;
  }

  const bookmarkForm = bookmarkDialog.querySelector('[data-bookmark-save-form]');
  if (!(bookmarkForm instanceof HTMLFormElement)) {
    return { destroy: () => {} };
  }

  const groupDialog = host.querySelector(groupDialogSelector);
  const groupForm = groupDialog instanceof HTMLDialogElement
    ? groupDialog.querySelector('[data-bookmark-group-save-form]')
    : null;
  const hasGroupDialog = groupDialog instanceof HTMLDialogElement && groupForm instanceof HTMLFormElement;
  const abortController = new AbortController();
  const { signal } = abortController;
  const timerIds = [];

  const documentRoot = document.documentElement;
  const csrfKey = String(documentRoot.dataset.csrfKey || '').trim();
  const csrfToken = String(documentRoot.dataset.csrfToken || '').trim();
  const moduleContext = { module: 'bookmark-save' };

  if (!csrfKey || !csrfToken) {
    warnOnce('UI_DATA_MISSING', 'Missing CSRF metadata for bookmark dialog', moduleContext);
    return { destroy: () => {} };
  }

  const nameInput = bookmarkForm.querySelector('input[name="name"]');
  const groupSelect = bookmarkForm.querySelector('[data-bookmark-group-select]');
  const newGroupBtn = bookmarkForm.querySelector('[data-bookmark-new-group]');
  const closeBookmarkBtns = bookmarkDialog.querySelectorAll('[data-bookmark-dialog-close]');
  const submitBtn = bookmarkDialog.querySelector('[data-bookmark-submit-label]');
  const titleEl = bookmarkDialog.querySelector('#app-bookmark-dialog-title');
  const urlPreviewEl = bookmarkDialog.querySelector('[data-bookmark-url-preview]');
  const nameErrorEl = bookmarkForm.querySelector('[data-field-error="name"]');

  const maxBookmarkMessage = bookmarkDialog.dataset.maxMessage || '';
  const bookmarkMessageSaved = bookmarkDialog.dataset.msgSaved || 'Bookmark saved';
  const bookmarkMessageUpdated = bookmarkDialog.dataset.msgUpdated || 'Bookmark updated';
  const bookmarkMessageErrorGeneric = bookmarkDialog.dataset.msgErrorGeneric || 'Bookmark action failed';
  const bookmarkMessageNameRequired = bookmarkDialog.dataset.msgNameRequired || 'Bookmark name is required';
  const labelSaving = bookmarkDialog.dataset.labelSaving || 'Saving...';
  const urlPreviewLabel = bookmarkDialog.dataset.urlPreviewLabel || 'Bookmarking this page:';
  const bookmarkApiErrorMessages = {
    name_required: bookmarkDialog.dataset.msgNameRequired || bookmarkMessageErrorGeneric,
    url_required: bookmarkDialog.dataset.msgUrlRequired || bookmarkMessageErrorGeneric,
    group_not_found: bookmarkDialog.dataset.msgGroupNotFound || bookmarkMessageErrorGeneric,
    not_found: bookmarkMessageErrorGeneric,
    create_failed: bookmarkMessageErrorGeneric,
    update_failed: bookmarkMessageErrorGeneric
  };

  let editingBookmarkId = null;
  let bookmarkReturnState = null;
  let savedSubmitLabel = '';

  let groupEditingId = null;
  let groupMode = 'create';
  let groupSource = 'sidebar';

  // --- Inline validation helpers ---

  const showFieldError = (input, errorEl, message) => {
    if (errorEl instanceof HTMLElement) {
      errorEl.textContent = message;
    }
    if (input instanceof HTMLElement) {
      input.setAttribute('aria-invalid', 'true');
    }
  };

  const clearFieldError = (input, errorEl) => {
    if (errorEl instanceof HTMLElement) {
      errorEl.textContent = '';
    }
    if (input instanceof HTMLElement) {
      input.removeAttribute('aria-invalid');
    }
  };

  // Clear inline error when user starts typing
  if (nameInput instanceof HTMLElement) {
    nameInput.addEventListener('input', () => {
      clearFieldError(nameInput, nameErrorEl);
    }, { signal });
  }

  // --- URL preview ---

  const updateUrlPreview = (show) => {
    if (!(urlPreviewEl instanceof HTMLElement)) return;
    if (show) {
      const url = currentRelativeUrl();
      urlPreviewEl.textContent = urlPreviewLabel + ' ' + (url || '/');
      urlPreviewEl.hidden = false;
    } else {
      urlPreviewEl.hidden = true;
      urlPreviewEl.textContent = '';
    }
  };

  // --- Core helpers ---

  const syncModalBodyClass = () => {
    const isOpen = bookmarkDialog.open || (hasGroupDialog && groupDialog.open);
    document.body.classList.toggle('modal-is-open', isOpen);
  };

  const reportError = (code, message, details = {}) => {
    warnOnce(code, message, { ...moduleContext, ...details });
    showAsyncFlash('error', message);
  };

  const resolveBookmarkApiErrorMessage = (errorCode) => {
    if (!errorCode) return bookmarkMessageErrorGeneric;
    return bookmarkApiErrorMessages[errorCode] || bookmarkMessageErrorGeneric;
  };

  const runReloadWithSuccess = (message) => {
    showAsyncFlash('success', message, 2500);
    const timerId = window.setTimeout(() => window.location.reload(), 450);
    timerIds.push(timerId);
  };

  const closeBookmarkDialog = () => {
    try {
      if (bookmarkDialog.open) bookmarkDialog.close();
    } catch {
      // no-op
    }
    bookmarkForm.removeAttribute('aria-busy');
    clearFieldError(nameInput, nameErrorEl);
    syncModalBodyClass();
  };

  const openBookmarkDialog = () => {
    try {
      bookmarkDialog.showModal();
      syncModalBodyClass();
      return true;
    } catch (error) {
      reportError('UI_DIALOG_OPEN_FAILED', bookmarkMessageErrorGeneric, { error });
      return false;
    }
  };

  const setBookmarkSubmitPending = (pending) => {
    if (submitBtn instanceof HTMLButtonElement) {
      if (pending) {
        savedSubmitLabel = submitBtn.textContent || '';
        submitBtn.textContent = labelSaving;
        submitBtn.disabled = true;
      } else {
        submitBtn.textContent = savedSubmitLabel || submitBtn.textContent;
        submitBtn.disabled = false;
      }
    }
    bookmarkForm.setAttribute('aria-busy', pending ? 'true' : 'false');
  };

  const setBookmarkSubmitLabel = (label) => {
    if (!(submitBtn instanceof HTMLElement)) return;
    const text = String(label || '').trim();
    if (!text) return;
    submitBtn.textContent = text;
  };

  const applyBookmarkModeUi = (isEditMode) => {
    if (titleEl) {
      titleEl.textContent = isEditMode
        ? (bookmarkDialog.dataset.titleEdit || 'Edit bookmark')
        : (bookmarkDialog.dataset.titleCreate || 'Add bookmark');
    }
    setBookmarkSubmitLabel(isEditMode
      ? (bookmarkDialog.dataset.labelUpdate || 'Save changes')
      : (bookmarkDialog.dataset.labelSave || 'Add bookmark'));
    updateUrlPreview(!isEditMode);
  };

  const configureBookmarkModeFromTopbar = () => {
    clearFieldError(nameInput, nameErrorEl);

    if (!(openBtn instanceof HTMLButtonElement)) {
      applyBookmarkModeUi(false);
      editingBookmarkId = null;
      bookmarkForm.reset();
      if (nameInput) {
        const title = document.title.replace(/\s*[|–—].*$/, '').trim();
        nameInput.value = title || '';
      }
      return;
    }

    const isEditMode = openBtn.hasAttribute('data-bookmark-existing-id');
    applyBookmarkModeUi(isEditMode);

    if (isEditMode) {
      editingBookmarkId = openBtn.dataset.bookmarkExistingId || null;
      if (nameInput) nameInput.value = openBtn.dataset.bookmarkExistingName || '';
      if (groupSelect) groupSelect.value = openBtn.dataset.bookmarkExistingGroup || '';
      return;
    }

    editingBookmarkId = null;
    bookmarkForm.reset();
    if (nameInput) {
      const title = document.title.replace(/\s*[|–—].*$/, '').trim();
      nameInput.value = title || '';
    }
  };

  const openBookmarkEditFromSidebar = (detail) => {
    const bookmarkId = Number.parseInt(String(detail?.id ?? ''), 10);
    const bookmarkName = String(detail?.name ?? '').trim();
    const groupId = String(detail?.groupId ?? '').trim();
    if (bookmarkId <= 0 || !bookmarkName) {
      reportError('UI_INVALID_STATE', bookmarkMessageErrorGeneric, { detail });
      return;
    }

    editingBookmarkId = String(bookmarkId);
    applyBookmarkModeUi(true);
    if (nameInput instanceof HTMLInputElement) {
      nameInput.value = bookmarkName;
    }
    if (groupSelect instanceof HTMLSelectElement) {
      groupSelect.value = groupId;
    }

    if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
      nameInput.focus();
      nameInput.select();
    }
  };

  const captureBookmarkState = () => ({
    isEditMode: editingBookmarkId !== null && editingBookmarkId !== '',
    bookmarkId: editingBookmarkId,
    name: nameInput instanceof HTMLInputElement ? nameInput.value : '',
    groupId: groupSelect instanceof HTMLSelectElement ? groupSelect.value : ''
  });

  const restoreBookmarkFromState = (selectedGroupId = null) => {
    const state = bookmarkReturnState;
    bookmarkReturnState = null;
    if (!state) {
      return;
    }

    applyBookmarkModeUi(Boolean(state.isEditMode));
    editingBookmarkId = state.isEditMode ? state.bookmarkId : null;
    if (nameInput instanceof HTMLInputElement) {
      nameInput.value = state.name || '';
    }
    if (groupSelect instanceof HTMLSelectElement) {
      groupSelect.value = selectedGroupId ?? state.groupId ?? '';
    }

    if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
      nameInput.focus();
      nameInput.select();
    }
  };

  // Bookmark dialog open
  if (openBtn instanceof HTMLButtonElement) {
    openBtn.addEventListener('click', () => {
      configureBookmarkModeFromTopbar();
      if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
        nameInput.select();
      }
    }, { signal });
  }

  document.addEventListener('app:bookmark-edit', (event) => {
    const detail = event instanceof CustomEvent ? event.detail : null;
    openBookmarkEditFromSidebar(detail);
  }, { signal });

  closeBookmarkBtns.forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.preventDefault();
      closeBookmarkDialog();
    }, { signal });
  });

  bookmarkDialog.addEventListener('click', (event) => {
    if (event.target === bookmarkDialog) {
      closeBookmarkDialog();
    }
  }, { signal });
  bookmarkDialog.addEventListener('close', () => {
    bookmarkForm.removeAttribute('aria-busy');
    syncModalBodyClass();
  }, { signal });
  bookmarkDialog.addEventListener('cancel', () => {
    bookmarkForm.removeAttribute('aria-busy');
    syncModalBodyClass();
  }, { signal });

  // Save bookmark
  bookmarkForm.addEventListener('submit', async (event) => {
    event.preventDefault();

    const name = nameInput instanceof HTMLInputElement ? nameInput.value.trim() : '';
    if (!name) {
      showFieldError(nameInput, nameErrorEl, bookmarkMessageNameRequired);
      if (nameInput instanceof HTMLInputElement) nameInput.focus();
      return;
    }

    const body = new FormData();
    body.set(csrfKey, csrfToken);
    body.set('name', name);
    body.set('group_id', groupSelect instanceof HTMLSelectElement ? groupSelect.value : '');

    setBookmarkSubmitPending(true);
    if (editingBookmarkId) {
      body.set('id', editingBookmarkId);
      try {
        const json = await postForm(getAppBase() + 'bookmarks/update-data', body);
        if (!json.ok) {
          reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
          return;
        }

        closeBookmarkDialog();
        runReloadWithSuccess(bookmarkMessageUpdated);
      } catch (error) {
        reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error, status: error?.status });
      } finally {
        setBookmarkSubmitPending(false);
      }
      return;
    }

    body.set('url', currentRelativeUrl());
    try {
      const json = await postForm(getAppBase() + 'bookmarks/save-data', body);
      if (!json.ok) {
        if (json.error === 'max_reached') {
          showAsyncFlash('warning', maxBookmarkMessage || 'Maximum bookmarks reached');
          return;
        }
        reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
        return;
      }

      closeBookmarkDialog();
      runReloadWithSuccess(json.mode === 'updated' ? bookmarkMessageUpdated : bookmarkMessageSaved);
    } catch (error) {
      reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error, status: error?.status });
    } finally {
      setBookmarkSubmitPending(false);
    }
  }, { signal });

  const destroy = () => {
    abortController.abort();
    timerIds.forEach((timerId) => window.clearTimeout(timerId));
    timerIds.length = 0;
    bookmarkForm.removeAttribute('aria-busy');
    delete bookmarkDialog.dataset.bookmarkSaveBound;
    delete bookmarkDialog._bookmarkSaveApi;
  };
  const api = { destroy };
  bookmarkDialog.dataset.bookmarkSaveBound = '1';
  bookmarkDialog._bookmarkSaveApi = api;

  if (!hasGroupDialog) {
    return api;
  }

  const groupNameInput = groupForm.querySelector('input[name="name"]');
  const groupIconInput = groupForm.querySelector('input[name="icon"]');
  const groupIconPicker = groupForm.querySelector('[data-bookmark-group-icon-picker]');
  const groupCloseBtns = groupDialog.querySelectorAll('[data-bookmark-group-dialog-close]');
  const groupTitleEl = groupDialog.querySelector('#app-bookmark-group-dialog-title');
  const groupSubmitBtn = groupDialog.querySelector('[data-bookmark-group-submit-label]');
  const groupNameErrorEl = groupForm.querySelector('[data-field-error="group-name"]');

  const groupMessageCreated = groupDialog.dataset.msgCreated || 'Group created';
  const groupMessageUpdated = groupDialog.dataset.msgUpdated || 'Group updated';
  const groupMessageErrorGeneric = groupDialog.dataset.msgErrorGeneric || 'Group action failed';
  const groupMessageNameRequired = groupDialog.dataset.msgNameRequired || 'Group name is required';
  const maxGroupMessage = groupDialog.dataset.maxMessage || 'Maximum groups reached';
  const groupLabelSaving = groupDialog.dataset.labelSaving || 'Saving...';
  const groupApiErrorMessages = {
    name_required: groupMessageNameRequired,
    not_found: groupMessageErrorGeneric,
    create_failed: groupMessageErrorGeneric,
    update_failed: groupMessageErrorGeneric
  };

  let savedGroupSubmitLabel = '';

  const resolveGroupApiErrorMessage = (errorCode) => {
    if (!errorCode) return groupMessageErrorGeneric;
    return groupApiErrorMessages[errorCode] || groupMessageErrorGeneric;
  };

  // Clear group name inline error on input
  if (groupNameInput instanceof HTMLElement) {
    groupNameInput.addEventListener('input', () => {
      clearFieldError(groupNameInput, groupNameErrorEl);
    }, { signal });
  }

  const closeGroupDialog = () => {
    try {
      if (groupDialog.open) groupDialog.close();
    } catch {
      // no-op
    }
    groupForm.removeAttribute('aria-busy');
    clearFieldError(groupNameInput, groupNameErrorEl);
    syncModalBodyClass();
  };

  const openGroupDialog = () => {
    try {
      groupDialog.showModal();
      syncModalBodyClass();
      return true;
    } catch (error) {
      reportError('UI_DIALOG_OPEN_FAILED', groupMessageErrorGeneric, { error });
      return false;
    }
  };

  const setGroupSubmitPending = (pending) => {
    if (groupSubmitBtn instanceof HTMLButtonElement) {
      if (pending) {
        savedGroupSubmitLabel = groupSubmitBtn.textContent || '';
        groupSubmitBtn.textContent = groupLabelSaving;
        groupSubmitBtn.disabled = true;
      } else {
        groupSubmitBtn.textContent = savedGroupSubmitLabel || groupSubmitBtn.textContent;
        groupSubmitBtn.disabled = false;
      }
    }
    groupForm.setAttribute('aria-busy', pending ? 'true' : 'false');
  };

  const setGroupSubmitLabel = (label) => {
    if (!(groupSubmitBtn instanceof HTMLElement)) return;
    const text = String(label || '').trim();
    if (!text) return;
    groupSubmitBtn.textContent = text;
  };

  const setGroupIcon = (icon) => {
    const value = String(icon || '').trim() || 'bi-folder';
    if (groupIconInput instanceof HTMLInputElement) {
      groupIconInput.value = value;
    }
    if (groupIconPicker instanceof HTMLElement) {
      groupIconPicker.querySelectorAll('[data-icon]').forEach((item) => {
        const isActive = item.dataset.icon === value;
        item.classList.toggle('app-bookmark-icon-active', isActive);
        item.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      });
    }
  };

  const upsertGroupOption = (group) => {
    if (!(groupSelect instanceof HTMLSelectElement)) {
      return;
    }
    const id = String(group?.id ?? '').trim();
    const name = String(group?.name ?? '').trim();
    if (!id || !name) {
      return;
    }

    const existing = Array.from(groupSelect.options).find((option) => option.value === id);
    if (existing) {
      existing.textContent = name;
    } else {
      const option = document.createElement('option');
      option.value = id;
      option.textContent = name;
      groupSelect.appendChild(option);
    }
  };

  const configureGroupDialog = ({ mode, source, id = null, name = '', icon = 'bi-folder', restoreState = null }) => {
    groupMode = mode === 'edit' ? 'edit' : 'create';
    groupSource = source === 'bookmark' ? 'bookmark' : 'sidebar';
    groupEditingId = groupMode === 'edit' ? id : null;
    bookmarkReturnState = groupSource === 'bookmark' ? restoreState : null;

    groupForm.reset();
    clearFieldError(groupNameInput, groupNameErrorEl);
    setGroupIcon(icon);
    if (groupNameInput instanceof HTMLInputElement) {
      groupNameInput.value = String(name || '');
    }

    if (groupTitleEl instanceof HTMLElement) {
      groupTitleEl.textContent = groupMode === 'edit'
        ? (groupDialog.dataset.titleEdit || 'Edit group')
        : (groupDialog.dataset.titleCreate || 'New group');
    }
    setGroupSubmitLabel(groupMode === 'edit'
      ? (groupDialog.dataset.labelUpdate || 'Save changes')
      : (groupDialog.dataset.labelSave || 'Create group'));
  };

  const handleGroupCancel = () => {
    closeGroupDialog();
    if (groupSource === 'bookmark' && bookmarkReturnState) {
      restoreBookmarkFromState();
    } else {
      bookmarkReturnState = null;
    }
  };

  const openCreateGroupFromBookmarkDialog = () => {
    const state = captureBookmarkState();
    closeBookmarkDialog();
    configureGroupDialog({
      mode: 'create',
      source: 'bookmark',
      restoreState: state
    });
    if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
      groupNameInput.focus();
      groupNameInput.select();
    }
  };

  if (newGroupBtn instanceof HTMLElement) {
    newGroupBtn.addEventListener('click', (event) => {
      event.preventDefault();
      openCreateGroupFromBookmarkDialog();
    }, { signal });
  }

  if (groupIconPicker instanceof HTMLElement) {
    groupIconPicker.addEventListener('click', (event) => {
      const target = event.target instanceof HTMLElement ? event.target.closest('[data-icon]') : null;
      if (!(target instanceof HTMLElement)) return;
      event.preventDefault();
      setGroupIcon(target.dataset.icon || 'bi-folder');
    }, { signal });
  }

  groupCloseBtns.forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.preventDefault();
      handleGroupCancel();
    }, { signal });
  });

  groupDialog.addEventListener('click', (event) => {
    if (event.target === groupDialog) {
      handleGroupCancel();
    }
  }, { signal });
  groupDialog.addEventListener('cancel', (event) => {
    event.preventDefault();
    handleGroupCancel();
  }, { signal });
  groupDialog.addEventListener('close', () => {
    groupForm.removeAttribute('aria-busy');
    syncModalBodyClass();
  }, { signal });

  document.addEventListener('app:bookmark-group-edit', (event) => {
    const detail = event instanceof CustomEvent ? event.detail : null;
    const groupId = Number.parseInt(String(detail?.id ?? ''), 10);
    const groupName = String(detail?.name ?? '').trim();
    const groupIcon = String(detail?.icon ?? 'bi-folder').trim() || 'bi-folder';
    if (groupId <= 0 || !groupName) {
      reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { detail });
      return;
    }

    configureGroupDialog({
      mode: 'edit',
      source: 'sidebar',
      id: groupId,
      name: groupName,
      icon: groupIcon
    });
    if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
      groupNameInput.focus();
      groupNameInput.select();
    }
  }, { signal });

  groupForm.addEventListener('submit', async (event) => {
    event.preventDefault();

    const name = groupNameInput instanceof HTMLInputElement ? groupNameInput.value.trim() : '';
    if (!name) {
      showFieldError(groupNameInput, groupNameErrorEl, groupMessageNameRequired);
      if (groupNameInput instanceof HTMLInputElement) groupNameInput.focus();
      return;
    }
    const icon = groupIconInput instanceof HTMLInputElement ? groupIconInput.value : 'bi-folder';

    const body = new FormData();
    body.set(csrfKey, csrfToken);
    body.set('name', name);
    body.set('icon', icon);
    if (groupMode === 'edit' && groupEditingId) {
      body.set('id', String(groupEditingId));
    }

    setGroupSubmitPending(true);
    try {
      const json = await postForm(getAppBase() + 'bookmarks/group-save-data', body);
      if (!json.ok) {
        if (json.error === 'max_reached') {
          showAsyncFlash('warning', maxGroupMessage);
          return;
        }
        reportError('FETCH_FAILED', resolveGroupApiErrorMessage(json.error), { error: json.error });
        return;
      }
      if (!json.group) {
        reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { reason: 'missing_group' });
        return;
      }

      const mode = json.mode === 'updated' ? 'updated' : 'created';
      const successMessage = mode === 'updated' ? groupMessageUpdated : groupMessageCreated;

      closeGroupDialog();
      if (groupSource === 'bookmark') {
        upsertGroupOption(json.group);
        restoreBookmarkFromState(String(json.group.id));
        showAsyncFlash('success', successMessage, 2000);
      } else {
        runReloadWithSuccess(successMessage);
      }
    } catch (error) {
      reportError('FETCH_ERROR', groupMessageErrorGeneric, { error, status: error?.status });
    } finally {
      setGroupSubmitPending(false);
    }
  }, { signal });

  return api;
}

function currentRelativeUrl() {
  const basePath = new URL(getAppBase()).pathname;
  const full = window.location.pathname + window.location.search;
  if (full.startsWith(basePath)) {
    return full.slice(basePath.length);
  }
  return full.replace(/^\//, '');
}
