// Auto-extracted from apps-manager.js (verbatim) — augments AppsManager.prototype. Loaded after the base.
Object.assign(AppsManager.prototype, {
  // Snapshot of CFG_ field values, keyed by input name. Mirrors the filter in
  // collectConfigFromForm so the two agree on what counts as a config field.
  _readConfigFieldState(form) {
    const state = {};
    form.querySelectorAll('input, select, textarea').forEach((input) => {
      const name = input.name;
      if (!name || !name.startsWith('CFG_') || name.endsWith('_PORT_MANAGER')) return;
      state[name] = (input.type === 'checkbox') ? (input.checked ? 'true' : 'false') : input.value;
    });
    return state;
  },
  _getDirtyConfigFields() {
    if (!this._dirtyAppName || !this._configSnapshot) return [];
    const form = document.getElementById(`app-form-${this._dirtyAppName}`);
    if (!form) return [];
    const current = this._readConfigFieldState(form);
    return Object.keys(current).filter((name) => current[name] !== (this._configSnapshot[name] ?? ''));
  },
  _isConfigDirty() {
    return this._getDirtyConfigFields().length > 0;
  },
  // Called once per config-panel render: snapshots the saved state, wires the
  // change listener + sticky bar, and (re)registers the nav guard. Only tracks
  // installed apps — for a fresh install the Install button is already the
  // "apply" action, so a dirty bar would just be noise.
  wireConfigDirtyTracking(appName) {
    const form = document.getElementById(`app-form-${appName}`);
    if (!form) return;

    const app = (window.apps || []).find((a) =>
      (a.command || '').endsWith(` ${appName}`) ||
      (a.name && a.name.toLowerCase() === appName.toLowerCase())
    );
    if (!app || !app.installed) {
      this._clearConfigDirty();
      return;
    }

    this._dirtyAppName = appName;
    this._configSnapshot = this._readConfigFieldState(form);

    if (form.dataset.dirtyWired !== '1') {
      form.dataset.dirtyWired = '1';
      const onEdit = () => this._refreshDirtyBar();
      form.addEventListener('input', onEdit);
      form.addEventListener('change', onEdit);
    }

    this._ensureDirtyBar(appName, form);

    // beforeunload covers tab close / refresh / external nav — the browser
    // shows its own generic prompt. Registered once for the page lifetime.
    if (!this._beforeUnloadWired) {
      this._beforeUnloadWired = true;
      window.addEventListener('beforeunload', (e) => {
        if (this._isConfigDirty()) { e.preventDefault(); e.returnValue = ''; }
      });
    }

    // SPA route changes route through this guard (see spa.js navigate()).
    window.__appConfigNavGuard = (targetPath) => this._appConfigNavGuard(targetPath);

    this._refreshDirtyBar();
  },
  // Build (or rebuild) the sticky bar at the bottom of the config form.
  _ensureDirtyBar(appName, form) {
    const stale = document.getElementById('config-dirty-bar');
    if (stale) stale.remove();

    const bar = document.createElement('div');
    bar.id = 'config-dirty-bar';
    bar.className = 'config-dirty-bar';
    bar.style.display = 'none';
    bar.innerHTML = `
      <span class="config-dirty-msg">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
          <line x1="12" y1="9" x2="12" y2="13"></line>
          <line x1="12" y1="17" x2="12.01" y2="17"></line>
        </svg>
        <span class="config-dirty-count"></span>
      </span>
      <span class="config-dirty-actions">
        <button type="button" class="btn btn-secondary config-dirty-discard">Discard</button>
        <button type="button" class="btn btn-primary config-dirty-apply">Apply</button>
      </span>`;
    // Sits in normal flow between the config content and the action buttons.
    const actions = form.querySelector('.config-actions');
    if (actions) {
      form.insertBefore(bar, actions);
    } else {
      form.appendChild(bar);
    }

    bar.querySelector('.config-dirty-discard').addEventListener('click', () => this._discardConfigChanges());
    bar.querySelector('.config-dirty-apply').addEventListener('click', () => this.installApp(appName));
  },
  _refreshDirtyBar() {
    const bar = document.getElementById('config-dirty-bar');
    if (!bar) return;
    const count = this._getDirtyConfigFields().length;
    if (count === 0) {
      bar.style.display = 'none';
      return;
    }
    const label = bar.querySelector('.config-dirty-count');
    if (label) label.textContent = `${count} unsaved change${count === 1 ? '' : 's'}`;
    bar.style.display = 'flex';
  },
  // Revert every field to its snapshot value, then re-fire change/input so
  // dependent UI (showWhen visibility, etc.) reconciles.
  _discardConfigChanges() {
    if (!this._dirtyAppName || !this._configSnapshot) return;
    const form = document.getElementById(`app-form-${this._dirtyAppName}`);
    if (!form) return;
    form.querySelectorAll('input, select, textarea').forEach((input) => {
      const name = input.name;
      if (!name || !name.startsWith('CFG_') || name.endsWith('_PORT_MANAGER')) return;
      if (!(name in this._configSnapshot)) return;
      const orig = this._configSnapshot[name];
      if (input.type === 'checkbox') {
        input.checked = (orig === 'true');
      } else {
        input.value = orig;
      }
      input.dispatchEvent(new Event('input', { bubbles: true }));
      input.dispatchEvent(new Event('change', { bubbles: true }));
    });
    this._refreshDirtyBar();
  },
  // Drop the dirty state without touching the form — used when the changes are
  // being applied (the form is about to be replaced) or discarded on leave.
  _clearConfigDirty() {
    this._configSnapshot = null;
    this._dirtyAppName = null;
    window.__appConfigNavGuard = null;
    const bar = document.getElementById('config-dirty-bar');
    if (bar) bar.style.display = 'none';
  },
  // SPA nav guard body — returns 'proceed' | 'stay'. 'apply' kicks off the
  // normal apply flow and stays put (apply navigates to the tasks view itself).
  async _appConfigNavGuard() {
    if (!this._isConfigDirty()) return 'proceed';
    const appName = this._dirtyAppName;
    const decision = await this._confirmLeaveUnsaved(appName);
    if (decision === 'apply') {
      this.installApp(appName);
      return 'stay';
    }
    if (decision === 'discard') {
      this._clearConfigDirty();
      return 'proceed';
    }
    return 'stay';
  },
  // Apply / Discard / Stay prompt. Resolves with the chosen action; closing
  // via the X or backdrop resolves 'stay' (the safe default).
  _confirmLeaveUnsaved(appName) {
    let displayName = appName;
    const app = (window.apps || []).find((a) =>
      (a.command || '').endsWith(` ${appName}`) ||
      (a.name && a.name.toLowerCase() === appName.toLowerCase())
    );
    if (app && app.name) displayName = app.name.split(' - ')[0].trim();

    return new Promise((resolve) => {
      let decided = false;
      const finish = (val, modal) => {
        if (decided) return;
        decided = true;
        if (modal) modal.close();
        resolve(val);
      };
      window.openEoModal({
        id: 'config-unsaved-modal',
        size: 'sm',
        eyebrow: '⚠  Unsaved changes',
        title: displayName,
        desc: 'You have configuration changes that haven’t been applied.',
        body: `
          <div class="eo-empty-state warning" role="status">
            <div class="eo-empty-state-body">
              <p class="eo-empty-state-title">Apply before you go?</p>
              <p class="eo-empty-state-text">Apply runs the update now. Discard throws the edits away. Stay keeps you on this page.</p>
            </div>
          </div>`,
        actions: [
          { label: 'Apply', variant: 'primary', onClick: (m) => finish('apply', m) },
          { label: 'Discard', variant: 'secondary', onClick: (m) => finish('discard', m) },
          { label: 'Stay', variant: 'secondary', onClick: (m) => finish('stay', m) }
        ],
        onClose: () => { if (!decided) { decided = true; resolve('stay'); } }
      });
    });
  },
});
