// Auto-extracted from apps-manager.js (verbatim) — augments AppsManager.prototype. Loaded after the base.
Object.assign(AppsManager.prototype, {
  async loadAppConfig(appName) {
    //// // console.log(`AppsManager: Loading config for ${appName}...`);
    
    try {
      // Get app data from global apps array (like original app-config-original.js)
      const appData = window.apps?.find(a => a.name === appName || a.command.includes(appName));
      
      if (appData && appData.config) {
        //// // console.log(`AppsManager: Loaded config for ${appName}:`, appData.config);
        
        // Update form with actual configuration values from app.config
        this.updateConfigForm(appName, appData.config);
      } else {
        //// // console.log(`AppsManager: No config found for ${appName}, showing default configuration`);
        
        // Show default configuration when no config exists
        this.updateConfigForm(appName, {
          CFG_APP_NAME: appData?.name || appName,
          CFG_VERSION: appData?.version || '1.0.0',
          CFG_PORT: '8080',
          CFG_DOMAIN: '',
          CFG_USERNAME: 'admin',
          CFG_PASSWORD: '',
          CFG_DEBUG: 'false',
          CFG_LOG_LEVEL: 'INFO'
        });
      }
    } catch (error) {
      //// // console.log(`AppsManager: Error loading config for ${appName}:`, error);
      
      // Get app data for defaults
      const appData = window.apps?.find(a => a.name === appName || a.command.includes(appName));
      
      // Show default configuration on error
      this.updateConfigForm(appName, {
        CFG_APP_NAME: appData?.name || appName,
        CFG_VERSION: appData?.version || '1.0.0',
        CFG_PORT: '8080',
        CFG_DOMAIN: '',
        CFG_USERNAME: 'admin',
        CFG_PASSWORD: '',
        CFG_DEBUG: 'false',
        CFG_LOG_LEVEL: 'INFO'
      });
    }
  },
  updateConfigForm(appName, appConfig) {
    const form = document.getElementById(`app-form-${appName}`);
    if (!form) return;

    const appData = window.apps?.find(a => a.name === appName || a.command?.includes(appName));

    Object.entries(appConfig).forEach(([key, value]) => {
      const field = form.querySelector(`[name="${key}"]`);
      if (!field) return;
      let nextValue = value;
      if (key.endsWith('_NETWORK')) {
        nextValue = this.applyContextualDefault('NETWORK', value, appData);
      }
      if (field.type === 'checkbox') {
        field.checked = nextValue === 'true' || nextValue === 'yes';
      } else {
        field.value = nextValue;
      }
    });
  },
  // Display configuration form (working method from app-config-original.js)
  async displayConfigForm(appData, preferredCategory = null) {
    //// // console.log('displayConfigForm called with:', appData, 'preferredCategory:', preferredCategory);
    const configSection = document.getElementById('config-section');
    if (!configSection) {
      return;
    }

    const cleanAppName = appData.command.split(' ').pop();

    const requiresKey = Object.keys(appData.config || {}).find(k => k.endsWith('_REQUIRES_SERVICE'));
    const requiredService = requiresKey ? (appData.config[requiresKey] || '').trim() : '';
    if (requiredService && !this.checkServiceInstalled(requiredService) && !appData.installed) {
      const slug = requiredService.toLowerCase();
      const serviceLabel = slug.charAt(0).toUpperCase() + slug.slice(1);
      const iconUrl = `/core/icons/apps/${encodeURIComponent(slug)}.svg`;
      configSection.innerHTML = `
        <div class="config-title">
          <h3>🛠️ Configuration Settings</h3>
          <p>Configure ${this.escHtml(appData.name)} to match your requirements</p>
        </div>
        <div class="dep-required-card" data-service="${this.escAttr(slug)}">
          <img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg';">
          <div class="dep-required-body">
            <div class="dep-required-title">${this.escHtml(serviceLabel)} required</div>
            <div class="dep-required-reason">${this.escHtml(serviceLabel)} needs to be installed before you can configure ${this.escHtml(appData.name)}.</div>
          </div>
          <button type="button" class="btn btn-primary dep-required-action" onclick="appsManager.navigateToServiceApp('${this.escAttr(slug)}')">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
              <polyline points="17,21 17,13 7,13 7,21"></polyline>
              <polyline points="7,3 7,8 15,8"></polyline>
            </svg>
            Install ${this.escHtml(serviceLabel)}
          </button>
        </div>
      `;
      return;
    }

    //// // console.log('Setting config form HTML for:', appData.name);

    // Generate simple tabbed interface with preferred category
    //// // console.log('🎨 Generating config HTML with working approach...');
    const tabsContent = await this.generateSimpleTabsAndContent(appData, preferredCategory);
    
    const configHTML = `
    <div class="config-title">
      <h3>🛠️ Configuration Settings</h3>
      <p>Configure ${appData.name} to match your requirements</p>
    </div>
    
    <form id="app-form-${cleanAppName}" class="config-form" onsubmit="return false;">
      <div class="config-container">
        <div class="tabs-wrapper">
          <div class="tabs-list" id="tabs-list-${Date.now()}">
            ${tabsContent.tabsHTML}
          </div>
          
          <div class="tabs-content">
            ${tabsContent.contentHTML}
          </div>
        </div>
      </div>
      
      <div class="config-actions">
        <button type="button" class="btn btn-secondary" onclick="appsManager.showAppsList('all')">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="19" y1="12" x2="5" y2="12"></line>
            <polyline points="12 19 5 12 12 5"></polyline>
          </svg>
          Back to Apps
        </button>

        <button type="button" class="btn ${appData.installed ? 'btn-manage' : 'btn-install'}" onclick="appsManager.installApp('${cleanAppName}')">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
            <polyline points="7 10 12 15 17 10"></polyline>
            <line x1="12" y1="15" x2="12" y2="3"></line>
          </svg>
          ${appData.installed ? 'Update Configuration' : 'Install App'}
        </button>

        ${appData.installed && cleanAppName !== 'libreportal' ? `
        <button type="button" class="btn btn-uninstall" onclick="appsManager.uninstallApp('${cleanAppName}')">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <polyline points="3,6 5,6 21,6"></polyline>
            <path d="m19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"></path>
            <line x1="10" y1="11" x2="10" y2="17"></line>
            <line x1="14" y1="11" x2="14" y2="17"></line>
          </svg>
          Uninstall App
        </button>
        ` : ''}

        ${appData.installed && cleanAppName === 'gluetun' ? `
        <button type="button" class="btn btn-secondary" onclick="appsManager.openGluetunRouteAppsModal()">
          <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="M12 22s8-4 8-12V5l-8-3-8 3v5c0 8 8 12 8 12z"/>
          </svg>
          Route Apps
        </button>
        ` : ''}
      </div>
    </form>
  `;
  
    configSection.innerHTML = configHTML;
  
    // Initialize tab functionality
    this.initializeSimpleTabs();
    
    // Enhance scrollbar dynamically
    this.enhanceTabsScrollbar();
  
    //// // console.log('Config form HTML set successfully');
  },
  // Generate simple tabs and content together (clean, reliable approach)
  async generateSimpleTabsAndContent(appData, preferredCategory = null) {
    //// // console.log('🏷️📄 generateSimpleTabsAndContent called');
    const categories = await this.getConfigCategories();
    const fieldMappings = await this.getFieldMappings();
    const appConfig = appData.config || {};
    
    //// // console.log(`🏷️ Config categories loaded:`, categories);
    //// // console.log(`🏷️ Field mappings loaded:`, fieldMappings);
    //// // console.log(`🏷️ App config:`, appConfig);
    //// // console.log(`🏷️ Preferred category:`, preferredCategory);
    
    //// // console.log('📂 Available categories:', Object.keys(categories));
    //// // console.log('🗂️ Available field mappings:', Object.keys(fieldMappings));
    //// // console.log('🔍 Looking for PORT_MANAGER in field mappings:', 'PORT_MANAGER' in fieldMappings);
    if ('PORT_MANAGER' in fieldMappings) {
      //// // console.log('🔍 PORT_MANAGER field config:', fieldMappings['PORT_MANAGER']);
    }
    
    let tabsHTML = '';
    let contentHTML = '';

    // Sort categories by order
    const sortedCategories = Object.entries(categories)
      .sort(([,a], [,b]) => a.order - b.order);

    // Render each category's fields up front and keep only the ones that
    // actually produced fields. A "hasFields" heuristic used to gate the tabs
    // separately, but it drifted from what generateConfigFields really emits —
    // so a category like Network could pass the check yet render empty, leaving
    // a blank tab whose body just reads "No configuration options available".
    // Trusting the rendered output keeps tabs and content in lockstep.
    const renderedCategories = [];
    for (const [key, category] of sortedCategories) {
      const content = await this.generateConfigFields(key, appData);
      if (!content || content.includes('class="no-fields"')) continue;
      renderedCategories.push({ key, category, content });
    }

    // Use the preferred category if it's one that has fields, else the first.
    const activeTab = (preferredCategory && renderedCategories.some(c => c.key === preferredCategory))
      ? preferredCategory
      : (renderedCategories[0] ? renderedCategories[0].key : null);

    for (const { key, category, content } of renderedCategories) {
      const isActive = key === activeTab ? 'active' : '';

      tabsHTML += `
          <button class="tab-button ${isActive}" data-tab="${key}" onclick="appsManager.showTab('${key}')">
            <span class="tab-emoji">${category.icon}</span>
            <span class="tab-name">${category.name}</span>
          </button>
        `;

      contentHTML += `
          <div class="tab-panel ${isActive}" id="panel-${key}">
            <div class="panel-header">
              <h4>${category.icon} ${category.name}</h4>
              <p>${category.description}</p>
            </div>
            <div class="panel-fields app-config">
              ${content}
            </div>
          </div>
        `;
    }

    return { tabsHTML, contentHTML };
  },
  // Initialize simple tabs (working method from app-config-original.js)
  initializeSimpleTabs() {
    //// // console.log('Simple tabs initialized');
  },
  // Generate simple fields (working method from app-config-original.js)
  async generateSimpleFields(categoryKey, appData) {
    //// // console.log(`🔧 Generating fields for category: ${categoryKey}`);
    const fieldMappings = await this.getFieldMappings();
    const appConfig = appData.config || {};
    let fieldsHTML = '';
    let hiddenFieldsHTML = '';

    // Find fields that belong to this category
    for (const [fieldKey, fieldConfig] of Object.entries(fieldMappings)) {
      if (fieldConfig.category === categoryKey) {
        //// // console.log(`🔧 Processing field: ${fieldKey} with config:`, fieldConfig);
        const cfgKey = this.findMatchingCFGKey(fieldKey, appConfig);

        // Skip generic mappings when a longer/more-specific one binds to the same cfgKey.
        if (cfgKey) {
          const moreSpecific = Object.keys(fieldMappings).some(otherKey =>
            otherKey !== fieldKey
            && otherKey.length > fieldKey.length
            && this.findMatchingCFGKey(otherKey, appConfig) === cfgKey
          );
          if (moreSpecific) continue;
        }
        //// // console.log(`🔧 Found CFG key: ${cfgKey} with value:`, appConfig[cfgKey]);
        
        // Special debug for PORT_1
        if (fieldKey === 'PORT_1') {
          //// // console.log(`🔧 PORT_1 DEBUG: fieldKey=${fieldKey}, cfgKey=${cfgKey}, hasValue=${!!appConfig[cfgKey]}, value="${appConfig[cfgKey]}"`);
          //// // console.log(`🔧 PORT_1 DEBUG: All app config keys:`, Object.keys(appConfig));
        }
        
        // For advanced tab, only show advanced fields
        if (categoryKey === 'advanced' && !fieldConfig.advanced) {
          continue; // Skip non-advanced fields in advanced tab
        }
        
        // For regular tabs, skip advanced fields
        if (categoryKey !== 'advanced' && fieldConfig.advanced) {
          continue; // Skip advanced fields in regular tabs
        }
        
        // Skip fields gated by category allowlist when this app's category
        // isn't in the list AND the override requirement isn't enabled.
        if (Array.isArray(fieldConfig.categoryAllowlist)) {
          const appCategory = String(appData?.category || '').toLowerCase();
          const inList = fieldConfig.categoryAllowlist.map(c => c.toLowerCase()).includes(appCategory);
          const override = fieldConfig.requirementOverride
            ? this.checkRequirementEnabled(fieldConfig.requirementOverride)
            : false;
          if (!inList && !override) continue;
        }

        // Generic requiresService gating from field-mapping JSON.
        if (fieldConfig.requiresService) {
          if (!this.checkServiceInstalled(fieldConfig.requiresService)) {
            const value = this.unmetDependencyValue(fieldConfig);
            fieldsHTML += this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value,
              fieldConfig.disabledReason || `${fieldConfig.requiresService} needs to be installed.`);
            continue;
          }
        }

        // Gate fields that depend on the global mail config being on.
        // Used for per-app email-notification toggles so a user can't
        // enable Email here without configuring SMTP under General first.
        if (fieldConfig.requiresGlobalMail) {
          const mailEnabled = await this.isGlobalMailEnabled();
          if (!mailEnabled) {
            const value = this.unmetDependencyValue(fieldConfig);
            fieldsHTML += this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value,
              fieldConfig.disabledReason || 'Configure mail in General settings first (CFG_MAIL_ENABLED=true).');
            continue;
          }
        }

        // Check conditional requirements for certain fields
        if (fieldKey === 'AUTHELIA' || fieldKey === 'HEADSCALE' || fieldKey === 'WHITELIST') {
          let serviceName;
          let isServiceInstalled;
          let disabledReason;

          if (fieldKey === 'AUTHELIA') {
            serviceName = 'authelia';
            isServiceInstalled = this.checkServiceInstalled(serviceName);
            disabledReason = 'Authelia needs to be installed';
          } else if (fieldKey === 'HEADSCALE') {
            serviceName = 'headscale';
            isServiceInstalled = this.checkServiceInstalled(serviceName);
            disabledReason = 'Headscale needs to be installed';
          } else if (fieldKey === 'WHITELIST') {
            serviceName = 'traefik';
            isServiceInstalled = this.checkServiceInstalled(serviceName);
            disabledReason = 'Traefik needs to be installed.';
          }

          if (!isServiceInstalled) {
            // Force off-state so a stored "true" can't render checked when the dep is missing.
            const value = this.unmetDependencyValue(fieldConfig);
            fieldsHTML += this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value, disabledReason);
            continue;
          }
        }
        
        // Get current value or use default
        let fieldValue = cfgKey && appConfig.hasOwnProperty(cfgKey) ? appConfig[cfgKey] : (fieldConfig.default || '');
        fieldValue = this.applyContextualDefault(fieldKey, fieldValue, appData);
        const fieldHTML = await this.generateField(fieldKey, cfgKey, fieldValue, fieldConfig);
        if (fieldConfig.hideByDefault) {
          hiddenFieldsHTML += fieldHTML;
        } else {
          fieldsHTML += fieldHTML;
        }
      }
    }

    if (hiddenFieldsHTML) {
      fieldsHTML += this.renderAdvancedToggleAndFields(hiddenFieldsHTML);
    }

    if (!fieldsHTML) {
      fieldsHTML = '<div class="no-fields">No configuration options available for this category.</div>';
    }

    return fieldsHTML;
  },
  // Wrap each hidden field as a direct grid sibling tagged .advanced-field so
  // they participate in the parent .panel-fields grid (continuing on the right
  // of the toggle) rather than reflowing into a nested grid.
  applyContextualDefault(fieldKey, value, appData) {
    if (fieldKey === 'NETWORK'
        && !appData?.installed
        && (value === 'default' || value === '')
        && this.checkServiceInstalled('gluetun')) {
      return 'gluetun';
    }
    return value;
  },
  renderAdvancedToggleAndFields(hiddenFieldsHTML) {
    const tagged = hiddenFieldsHTML.replace(/<div class="form-field/g, '<div class="form-field advanced-field is-hidden');
    return `
      <div class="form-field advanced-toggle-field">
        <label class="form-label">Advanced Settings</label>
        <label class="checkbox-label">
          <input type="checkbox" class="advanced-fields-checkbox">
          <div class="checkbox-custom"></div>
          <span class="checkbox-text">Show advanced settings</span>
        </label>
        <small class="form-help">Reveal less-common configuration options for power users.</small>
      </div>
      ${tagged}
    `;
  },
  // Generate field (working method from app-config-original.js)
  async generateField(fieldKey, cfgKey, value, fieldConfig) {
    const fieldId = fieldKey; // Use fieldKey to ensure unique IDs
    const required = fieldConfig.required ? '<span class="required">*</span>' : '';
    const helpIcon = fieldConfig.tooltip ? `<span class="help-icon" title="${this.escAttr(fieldConfig.tooltip)}">?</span>` : '';
    
    let inputHTML = '';
    
    // Special handling for DOMAIN fields - show domain dropdown
    if (fieldKey === 'DOMAIN') {
      //// // console.log('🎯 DOMAIN field detected, generating dropdown...');
      try {
        const domainOptions = await this.getDomainOptions();
        //// // console.log('📊 Domain options received:', domainOptions);
        let optionsHTML = '';
        
        domainOptions.forEach(option => {
          const isSelected = option.value === value.toString() ? 'selected' : '';
          optionsHTML += `<option value="${option.value}" ${isSelected}>${option.label}</option>`;
        });
        
        inputHTML = `<select id="${fieldId}" name="${cfgKey}" class="form-select">${optionsHTML}</select>`;
        //// // console.log('✅ Domain dropdown generated successfully');
      } catch (error) {
        console.error('❌ Error loading domain options, falling back to number input:', error);
        // Fallback to regular number input if domain loading fails
        inputHTML = `<input type="number" id="${fieldId}" name="${cfgKey}" value="${value}" class="form-input" min="${fieldConfig.min || '1'}" max="${fieldConfig.max || '9'}" step="${fieldConfig.step || '1'}">`;
      }
    } else if (fieldKey === 'GLUETUN_VPN_COUNTRIES') {
      const selected = (typeof value === 'string' ? value : '').split(',').map(s => s.trim()).filter(Boolean);
      const chips = selected.length
        ? selected.map(c => `<span class="gluetun-country-chip"><span class="gluetun-flag">${this.countryFlagEmoji(c)}</span>${c}</span>`).join('')
        : `<span class="gluetun-country-empty">Any</span>`;
      inputHTML = `
        <div class="gluetun-countries-field" id="${fieldId}-wrap">
          <div class="gluetun-countries-display" id="${fieldId}-chips">${chips}</div>
          <button type="button" class="btn btn-secondary gluetun-countries-edit" onclick="appsManager.openGluetunCountriesModal('${fieldId}')">Edit</button>
          <input type="hidden" id="${fieldId}" name="${cfgKey}" value="${value || ''}">
        </div>`;
    } else {
      // Regular field handling for all other types
      // Auto-detect PORT fields and use port manager
      if (fieldKey.startsWith('PORT_') || fieldConfig.type === 'port-manager') {
        // Special handling for port manager - will be initialized after DOM is ready
        //// // console.log(`🔌 Creating port-manager field: ${fieldKey} for app: ${this.getCurrentAppName()}`);
        inputHTML = `<div id="${fieldId}" class="port-manager-container" data-field-key="${fieldKey}" data-app-name="${this.getCurrentAppName()}">Loading port manager...</div>`;
      } else {
        switch (fieldConfig.type) {
          case 'text':
            inputHTML = `<input type="text" id="${fieldId}" name="${cfgKey}" value="${value}" class="form-input" placeholder="${fieldConfig.placeholder || ''}">`;
            break;
          case 'password': {
            const randomMatch = typeof value === 'string' && /^RANDOMIZEDPASSWORD\d+$/.test(value);
            if (randomMatch) {
              const placeholderToken = value;
              inputHTML = `
                <div class="password-mode-wrapper" data-field-id="${fieldId}" data-field-key="${cfgKey}" data-placeholder-token="${placeholderToken}" style="display: flex; gap: 6px; align-items: stretch;">
                  <select class="form-input password-mode-select" style="max-width: 130px;" onchange="appsManager.setPasswordMode('${fieldId}', this.value)">
                    <option value="random" selected>Random</option>
                    <option value="custom">Custom</option>
                  </select>
                  <div class="password-field-wrapper" style="flex: 1;">
                    <input type="password" id="${fieldId}" value="" class="form-input password-field" placeholder="Will generate on save" readonly>
                    <input type="hidden" id="${fieldId}-token" name="${cfgKey}" value="${placeholderToken}">
                    <button type="button" class="password-toggle" tabindex="-1" aria-label="Show password" onclick="appsManager.togglePasswordVisibility('${fieldId}')">
                      <span id="${fieldId}-icon" class="password-toggle-icon">👁</span>
                    </button>
                  </div>
                </div>`;
            } else {
              inputHTML = `
                <div class="password-field-wrapper">
                  <input type="password" id="${fieldId}" name="${cfgKey}" value="${value}" class="form-input password-field" placeholder="${fieldConfig.placeholder || ''}">
                  <button type="button" class="password-toggle" tabindex="-1" aria-label="Show password" onclick="appsManager.togglePasswordVisibility('${fieldId}')">
                    <span id="${fieldId}-icon" class="password-toggle-icon">👁</span>
                  </button>
                </div>`;
            }
            break;
          }
          case 'number':
            inputHTML = `<input type="number" id="${fieldId}" name="${cfgKey}" value="${value}" class="form-input" min="${fieldConfig.min || ''}" max="${fieldConfig.max || ''}" step="${fieldConfig.step || ''}">`;
            break;
          case 'select':
            let optionsHTML = '';
            let selectOptions = fieldConfig.options;
            if (!selectOptions && typeof ConfigOptions !== 'undefined' && ConfigOptions.isDropdownKey?.(cfgKey)) {
              selectOptions = ConfigOptions.getSelectOptions(cfgKey);
            }
            // Backup strategy: "live" is only valid for apps we can snapshot
            // consistently. Hide it elsewhere so we never offer a choice that
            // would just fall back to stopping the app.
            if (selectOptions && cfgKey.endsWith('_BACKUP_STRATEGY') && !this.isCurrentAppLiveCapable()) {
              selectOptions = selectOptions.filter(o => String(o.value) !== 'live');
            }
            // Fall back to default if stored value isn't in the option list.
            let effectiveValue = value;
            if (selectOptions && selectOptions.length > 0) {
              const hasMatch = selectOptions.some(o => String(o.value) === String(value));
              if (!hasMatch) {
                effectiveValue = (fieldConfig.default !== undefined && fieldConfig.default !== null)
                  ? fieldConfig.default
                  : selectOptions[0].value;
              }
            }
            if (selectOptions) {
              selectOptions.forEach(option => {
                const isSelected = String(option.value) === String(effectiveValue) ? 'selected' : '';
                optionsHTML += `<option value="${option.value}" ${isSelected}>${option.label}</option>`;
              });
            }
            inputHTML = `<select id="${fieldId}" name="${cfgKey}" class="form-select">${optionsHTML}</select>`;
            break;
          case 'checkbox':
            const isChecked = value === 'true' || value === true ? 'checked' : '';
            inputHTML = `
              <label class="checkbox-label">
                <input type="checkbox" id="${fieldId}" name="${cfgKey}" ${isChecked}>
                <div class="checkbox-custom"></div>
                <span class="checkbox-text">${fieldConfig.label}</span>
              </label>
            `;
            break;
          case 'textarea':
            inputHTML = `<textarea id="${fieldId}" name="${cfgKey}" class="form-textarea" rows="${fieldConfig.rows || 3}" placeholder="${fieldConfig.placeholder || ''}">${value}</textarea>`;
            break;
          default:
            inputHTML = `<input type="text" id="${fieldId}" name="${cfgKey}" value="${value}" class="form-input">`;
        }
      }
    }
    
    // Generic conditional field: only render-visible when another field's
    // current value matches. The post-render `wireShowWhenListeners` keeps
    // visibility in sync as the watched field changes. Schema:
    //   showWhen: { "<KEY>": "<expectedValue>" }
    // <KEY> can be either a full CFG_ name or a bare suffix like
    // "NOTIFY_EMAIL"; bare keys auto-resolve against the current field's
    // app prefix so the same field-mapping is reusable across apps.
    // For checkboxes the expected value is "true" or "false".
    let showWhenAttrs = '';
    let showWhenStyle = '';
    if (fieldConfig.showWhen && typeof fieldConfig.showWhen === 'object') {
      const entries = Object.entries(fieldConfig.showWhen);
      if (entries.length > 0) {
        let [watchKey, expected] = entries[0];
        // Auto-prefix bare keys with the current field's CFG_<APP>_ prefix.
        if (cfgKey && !String(watchKey).startsWith('CFG_')) {
          const m = String(cfgKey).match(/^(CFG_[A-Z0-9]+_)/);
          if (m) watchKey = `${m[1]}${watchKey}`;
        }
        const currentValue = this._readWatchedValue(watchKey);
        const visible = String(currentValue) === String(expected);
        showWhenAttrs = ` data-show-when-key="${watchKey}" data-show-when-equals="${String(expected)}"`;
        if (!visible) showWhenStyle = ' style="display: none;"';
      }
    }

    return `
      <div class="form-field"${showWhenAttrs}${showWhenStyle}>
        <label class="form-label" for="${fieldId}">
          ${fieldConfig.label}${required}${helpIcon}
        </label>
        ${inputHTML}
        ${fieldConfig.tooltip ? `<small class="form-help">${this.escHtml(fieldConfig.tooltip)}</small>` : ''}
      </div>
    `;
  },
  // Best-effort lookup of a watched field's current value during render.
  // Reads from the in-flight form (already-rendered fields above this one)
  // OR from the cached app config so the initial visibility is right even
  // for forward references.
  _readWatchedValue(cfgKey) {
    const live = document.querySelector(`[name="${cfgKey}"]`);
    if (live) {
      if (live.type === 'checkbox') return live.checked ? 'true' : 'false';
      return live.value;
    }
    const cached = this.currentAppConfig || {};
    if (Object.prototype.hasOwnProperty.call(cached, cfgKey)) {
      const v = cached[cfgKey];
      if (typeof v === 'boolean') return v ? 'true' : 'false';
      return String(v);
    }
    return '';
  },
  // Hook change events on every watched CFG_KEY and toggle dependent
  // .form-field[data-show-when-key=...] elements when the watched value
  // changes. Called after the config form is rendered.
  wireShowWhenListeners() {
    const dependents = document.querySelectorAll('.form-field[data-show-when-key]');
    if (dependents.length === 0) return;

    // Build a map: watchedKey -> [{element, expected}]
    const watch = new Map();
    dependents.forEach((el) => {
      const key = el.getAttribute('data-show-when-key');
      const expected = el.getAttribute('data-show-when-equals');
      if (!key) return;
      if (!watch.has(key)) watch.set(key, []);
      watch.get(key).push({ element: el, expected });
    });

    const evalKey = (key) => {
      const entry = watch.get(key);
      if (!entry) return;
      const input = document.querySelector(`[name="${key}"]`);
      let val = '';
      if (input) {
        val = input.type === 'checkbox' ? (input.checked ? 'true' : 'false') : input.value;
      }
      entry.forEach(({ element, expected }) => {
        element.style.display = String(val) === String(expected) ? '' : 'none';
      });
    };

    watch.forEach((_v, key) => {
      const input = document.querySelector(`[name="${key}"]`);
      if (!input || input.dataset.showWhenWired === '1') return;
      input.dataset.showWhenWired = '1';
      input.addEventListener('change', () => evalKey(key));
      input.addEventListener('input', () => evalKey(key));
      // Run once on init so any forward-reference defaults reconcile.
      evalKey(key);
    });

    // showWhen dependents render as the grid cell immediately after their
    // controller (generateConfigFields reorders them there), so revealing one
    // drops the input in the slot right next to its toggle.
  },
  // Generate configuration field HTML (from old file - needed for tab content)
  // Are any CFG_DOMAIN_N configured? The per-app DOMAIN field is just an
  // index into that list, so showing it when the list is empty is noise.
  // Refetched on every form render so changes on the config page are
  // reflected the next time an app's config tab opens.
  async hasConfiguredDomains() {
    try {
      const res = await fetch('/data/config/generated/configs.json', { cache: 'no-store' });
      if (!res.ok) return false;
      const json = await res.json();
      const flat = JSON.stringify(json);
      for (let i = 1; i <= 9; i++) {
        const m = flat.match(new RegExp(`"CFG_DOMAIN_${i}"\\s*:\\s*\\{[^}]*"value"\\s*:\\s*"([^"]*)"`));
        if (m && m[1].trim()) return true;
      }
      return false;
    } catch { return false; }
  },
  // Returns true if the user has switched on the global mail config.
  // Used by `requiresGlobalMail` field gating so per-app email-notification
  // toggles can refuse to enable until SMTP is configured once globally.
  async isGlobalMailEnabled() {
    try {
      const res = await fetch('/data/config/generated/configs.json', { cache: 'no-store' });
      if (!res.ok) return false;
      const json = await res.json();
      const v = json?.config?.CFG_MAIL_ENABLED?.value;
      return String(v).toLowerCase() === 'true';
    } catch { return false; }
  },
  async generateConfigFields(categoryKey, appData) {
    const fieldMappings = await this.getFieldMappings();
    const appConfig = appData.config || {};
    const domainsAvailable = await this.hasConfiguredDomains();
    let fieldsHTML = '';
    let hiddenFieldsHTML = '';

    // Collect every field that belongs to this category.
    const categoryFields = [];
    Object.entries(fieldMappings).forEach(([fieldKey, fieldConfig]) => {
      if (fieldConfig.category !== categoryKey) return;
      const cfgKey = this.findMatchingCFGKey(fieldKey, appConfig);

      // Advanced fields only on the advanced tab, and vice versa.
      if (categoryKey === 'advanced' && !fieldConfig.advanced) return;
      if (categoryKey !== 'advanced' && fieldConfig.advanced) return;

      // Only show a field if this app actually has the CFG_ variable.
      if (!cfgKey || !appConfig.hasOwnProperty(cfgKey)) return;

      // The DOMAIN selector is just an index into the domain list — hide it
      // entirely when no CFG_DOMAIN_N is configured.
      if (fieldKey === 'DOMAIN' && !domainsAvailable) return;

      // BACKUP gets priority -1 so "Enable Backups?" is always first; other
      // inputs are 0, remaining checkboxes 1.
      const isBackup = fieldKey === 'BACKUP';
      categoryFields.push({
        fieldKey,
        fieldConfig,
        cfgKey,
        priority: isBackup ? -1 : (fieldConfig.type === 'checkbox' ? 1 : 0)
      });
    });

    categoryFields.sort((a, b) => a.priority - b.priority);

    // The sort above orders by type (inputs before checkboxes), which can
    // separate a showWhen field from its controlling toggle. Reorder so each
    // dependent sits immediately after its controller — then its conditional
    // input reveals in the grid cell right next to the toggle.
    const byCfgKey = new Map(categoryFields.map(f => [f.cfgKey, f]));
    const resolveWatchKey = (entry) => {
      const sw = entry.fieldConfig.showWhen;
      if (!sw || typeof sw !== 'object') return null;
      const swEntries = Object.entries(sw);
      if (!swEntries.length) return null;
      let [watchKey] = swEntries[0];
      if (!String(watchKey).startsWith('CFG_')) {
        const m = String(entry.cfgKey).match(/^(CFG_[A-Z0-9]+_)/);
        if (m) watchKey = `${m[1]}${watchKey}`;
      }
      return watchKey;
    };

    const ordered = [];
    const placed = new Set();
    for (const entry of categoryFields) {
      if (placed.has(entry.cfgKey)) continue;
      // Dependents whose controller is in this category are placed alongside
      // their controller below — skip them in this outer pass.
      const watchKey = resolveWatchKey(entry);
      if (watchKey && byCfgKey.has(watchKey)) continue;

      ordered.push(entry);
      placed.add(entry.cfgKey);
      for (const dep of categoryFields) {
        if (placed.has(dep.cfgKey)) continue;
        if (resolveWatchKey(dep) === entry.cfgKey) {
          ordered.push(dep);
          placed.add(dep.cfgKey);
        }
      }
    }
    // Safety net: anything still unplaced (e.g. a dependent whose controller
    // lives in another category) keeps its original sorted position.
    for (const entry of categoryFields) {
      if (!placed.has(entry.cfgKey)) { ordered.push(entry); placed.add(entry.cfgKey); }
    }

    for (const entry of ordered) {
      const rendered = await this._renderCategoryField(entry, appData, appConfig);
      if (!rendered) continue;
      if (rendered.hidden) hiddenFieldsHTML += rendered.html;
      else fieldsHTML += rendered.html;
    }

    if (hiddenFieldsHTML) {
      fieldsHTML += this.renderAdvancedToggleAndFields(hiddenFieldsHTML);
    }

    if (!fieldsHTML) {
      fieldsHTML = '<div class="no-fields">No configuration options available for this category.</div>';
    }

    return fieldsHTML;
  },
  // Render a single collected category field: runs the dependency/service
  // gating, then produces the .form-field HTML. Returns { html, hidden } or
  // null when the field should be skipped entirely.
  async _renderCategoryField(entry, appData, appConfig) {
    const { fieldKey, fieldConfig, cfgKey } = entry;

    // Skip categoryAllowlist fields when this app's category isn't listed
    // AND the override requirement isn't enabled.
    if (Array.isArray(fieldConfig.categoryAllowlist)) {
      const appCategory = String(appData?.category || '').toLowerCase();
      const inList = fieldConfig.categoryAllowlist.map(c => c.toLowerCase()).includes(appCategory);
      const override = fieldConfig.requirementOverride
        ? this.checkRequirementEnabled(fieldConfig.requirementOverride)
        : false;
      if (!inList && !override) return null;
    }

    // Generic requiresService gating from the field-mapping JSON.
    if (fieldConfig.requiresService && !this.checkServiceInstalled(fieldConfig.requiresService)) {
      const value = appConfig.hasOwnProperty(cfgKey) ? appConfig[cfgKey] : (fieldConfig.default || '');
      return {
        html: this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value,
          fieldConfig.disabledReason || `${fieldConfig.requiresService} needs to be installed.`),
        hidden: false
      };
    }

    // requiresServices: ALL listed services must be installed (e.g. the
    // MONITORING toggle needs both prometheus and grafana).
    if (Array.isArray(fieldConfig.requiresServices)) {
      const missing = fieldConfig.requiresServices.filter(s => !this.checkServiceInstalled(s));
      if (missing.length) {
        const value = appConfig.hasOwnProperty(cfgKey) ? appConfig[cfgKey] : (fieldConfig.default || '');
        return {
          html: this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value,
            fieldConfig.disabledReason || `${missing.join(' + ')} needs to be installed.`),
          hidden: false
        };
      }
    }

    // Legacy hardcoded service checks for fields not yet migrated.
    if (fieldKey === 'AUTHELIA' || fieldKey === 'HEADSCALE' || fieldKey === 'WHITELIST') {
      let serviceName, disabledReason;
      if (fieldKey === 'AUTHELIA') { serviceName = 'authelia'; disabledReason = 'Authelia needs to be installed'; }
      else if (fieldKey === 'HEADSCALE') { serviceName = 'headscale'; disabledReason = 'Headscale needs to be installed'; }
      else { serviceName = 'traefik'; disabledReason = 'Traefik needs to be installed.'; }

      if (!this.checkServiceInstalled(serviceName)) {
        const value = appConfig.hasOwnProperty(cfgKey) ? appConfig[cfgKey] : (fieldConfig.default || '');
        return { html: this.generateDisabledField(fieldKey, fieldConfig, cfgKey, value, disabledReason), hidden: false };
      }
    }

    let fieldValue = appConfig[cfgKey] || (fieldConfig.default || '');
    fieldValue = this.applyContextualDefault(fieldKey, fieldValue, appData);
    const fieldHTML = await this.generateField(fieldKey, cfgKey, fieldValue, fieldConfig);
    return { html: fieldHTML, hidden: !!fieldConfig.hideByDefault };
  },
  // Generate configuration field HTML
  generateConfigField(cfgKey, value, fieldConfig) {
    const description = fieldConfig.description || '';
    let fieldHTML = `
      <div class="config-field">
        <label class="config-label">${cfgKey}</label>
    `;
    
    const type = fieldConfig.type || 'text';
    const options = fieldConfig.options;
    
    switch (type) {
      case 'text':
        fieldHTML += `<input type="text" id="${cfgKey}" name="${cfgKey}" value="${value}" class="config-input">`;
        break;
      case 'number':
        fieldHTML += `<input type="number" id="${cfgKey}" name="${cfgKey}" value="${value}" class="config-input">`;
        break;
      case 'password':
        fieldHTML += `
          <div class="password-field-wrapper">
            <input type="password" id="${cfgKey}" name="${cfgKey}" value="${value}" class="config-input password-field">
            <button type="button" class="password-toggle" tabindex="-1" aria-label="Show password" onclick="appsManager.togglePasswordVisibility('${cfgKey}')">
              <span id="${cfgKey}-icon" class="password-toggle-icon">👁</span>
            </button>
          </div>`;
        break;
      case 'checkbox':
        const checked = value === 'true' || value === 'yes' ? 'checked' : '';
        fieldHTML += `<input type="checkbox" id="${cfgKey}" name="${cfgKey}" ${checked} class="config-checkbox">`;
        break;
      case 'select':
        fieldHTML += `<select id="${cfgKey}" name="${cfgKey}" class="config-input">`;
        if (options) {
          options.forEach(option => {
            const selected = value === option ? 'selected' : '';
            fieldHTML += `<option value="${option}" ${selected}>${option}</option>`;
          });
        }
        fieldHTML += `</select>`;
        break;
      default:
        fieldHTML += `<input type="text" id="${cfgKey}" name="${cfgKey}" value="${value}" class="config-input">`;
    }
    
    fieldHTML += `
        </div>
        ${description ? `<p class="config-description">${description}</p>` : ''}
      </div>
    `;
    
    return fieldHTML;
  },
  // Show tab (working method from app-config-original.js)
  showTab(tabKey) {
    // Hide all panels
    const allPanels = document.querySelectorAll('.tab-panel');
    allPanels.forEach(panel => panel.classList.remove('active'));

    // Remove active from all config category tabs (not main navigation tabs)
    const allButtons = document.querySelectorAll('.tab-panel:has(.config-section) .tab-button, .config-section .tab-button');
    allButtons.forEach(button => button.classList.remove('active'));

    // Show selected panel
    const targetPanel = document.getElementById(`panel-${tabKey}`);
    if (targetPanel) {
      targetPanel.classList.add('active');
    }

    // Add active to clicked config category button
    const targetButton = document.querySelector(`.config-section [data-tab="${tabKey}"], .tab-panel:has(.config-section) [data-tab="${tabKey}"]`);
    if (targetButton) {
      targetButton.classList.add('active');
    }

    // Push the path-based URL so this sub-tab is shareable + back-buttonable —
    // /app/<name>/config/<sub>. Skipped when there's no current app (e.g. when
    // the form is rendered outside of the per-app context).
    const currentApp = window.appTabbedManager?.currentApp;
    if (currentApp && window.appPath) {
      const newUrl = window.appPath(currentApp, 'config', tabKey);
      if (window.location.pathname + window.location.search !== newUrl) {
        history.pushState({}, '', newUrl);
      }
    }
  },
  // Get navigation button for installing required services
  getNavigationButton(fieldKey) {
    const servicePages = {
      'AUTHELIA': '/app/authelia',
      'HEADSCALE': '/app/headscale',
      'WHITELIST': '/app/traefik',
      'TRAEFIK': '/app/traefik'
    };
    
    let serviceName;
    if (fieldKey === 'WHITELIST') {
      serviceName = 'Traefik';
    } else if (fieldKey === 'AUTHELIA') {
      serviceName = 'Authelia';
    } else if (fieldKey === 'HEADSCALE') {
      serviceName = 'Headscale';
    } else {
      serviceName = fieldKey.charAt(0) + fieldKey.slice(1).toLowerCase();
    }
    
    const pageUrl = servicePages[fieldKey] || '#';
    
    return `
      <button class="nav-button install-button" onclick="appsManager.handleNavigation('${pageUrl}', '${serviceName}')" data-service="${serviceName.toLowerCase()}">
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
          <polyline points="17,21 17,13 7,13 7,21"></polyline>
          <polyline points="7,3 7,8 15,8"></polyline>
        </svg>
        Install ${serviceName}
      </button>
    `;
  },
  // Handle navigation with unsaved changes check
  handleNavigation(url, serviceName) {
    // SPA in-app nav (path-based routes), with an absolute-path full-load
    // fallback. A relative window.location.href here resolved wrong from the
    // /admin/config/* pages these buttons render on.
    if (typeof window.navigateToRoute === 'function' && window.spaClean) {
      window.navigateToRoute(url);
    } else {
      window.location.href = url;
    }
  },
  // Generate disabled field with navigation button
  serviceForField(fieldKey, fieldConfig) {
    const map = { AUTHELIA: 'authelia', HEADSCALE: 'headscale', WHITELIST: 'traefik' };
    return (map[fieldKey] || fieldConfig.requiresService || '').toLowerCase();
  },
  generateDisabledField(fieldKey, fieldConfig, cfgKey, value, disabledReason) {
    const fieldId = fieldKey;
    const slug = this.serviceForField(fieldKey, fieldConfig);
    const serviceName = slug ? slug.charAt(0).toUpperCase() + slug.slice(1) : '';
    const iconUrl = slug ? `/core/icons/apps/${encodeURIComponent(slug)}.svg` : '/core/icons/apps/default.svg';
    const isCheckbox = fieldConfig.type === 'checkbox';
    const hiddenInput = isCheckbox
      ? `<input type="hidden" id="${fieldId}" name="${cfgKey}" value="false">`
      : `<input type="hidden" id="${fieldId}" name="${cfgKey}" value="${this.escAttr(value)}">`;

    return `
      <div class="dep-required-card" data-service="${this.escAttr(slug)}">
        ${hiddenInput}
        <img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg';">
        <div class="dep-required-body">
          <div class="dep-required-title">${this.escHtml(fieldConfig.label)}</div>
          <div class="dep-required-reason">${this.escHtml(disabledReason)}</div>
        </div>
        ${slug ? `<button type="button" class="btn btn-primary dep-required-action" onclick="appsManager.navigateToServiceApp('${this.escAttr(slug)}')">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
            <polyline points="17,21 17,13 7,13 7,21"></polyline>
            <polyline points="7,3 7,8 15,8"></polyline>
          </svg>
          Install ${this.escHtml(serviceName)}
        </button>` : ''}
      </div>
    `;
  },
  // Off-value for a checkbox whose dependency isn't installed.
  unmetDependencyValue(fieldConfig) {
    return fieldConfig.type === 'checkbox' ? 'false' : '';
  },
  // Helper methods to load config data (working methods from app-config-original.js)
  async getConfigCategories() {
    try {
      // Load config categories (for app config tabs)
      const response = await fetch('/data/apps/apps-config-categories.json');
      const data = await response.json();
      //// // console.log('✅ Loaded config categories from apps folder');
      return data.categories || data; // Return the actual data object
    } catch (error) {
      console.error('Error loading config categories:', error);
      throw new Error('Failed to load config categories. Please check your configuration files.');
    }
  },
  async getFieldMappings() {
    try {
      // Load from apps folder (static file)
      const response = await fetch('/data/apps/apps-field-mappings.json');
      const data = await response.json();
      //// // console.log('✅ Loaded field mappings from apps folder');
      return data.fields || data;
    } catch (error) {
      console.error('Error loading field mappings:', error);
      throw new Error('Failed to load field mappings. Please check your configuration files.');
    }
  },
  // Get domain options for DOMAIN field
  async getDomainOptions() {
    //// // console.log('🎯 Getting domain options...');
    
    try {
      //// // console.log('🔍 Starting domain fetch...');
      
      // Try to load system config to get domain information
      const response = await fetch('/data/config/generated/configs.json');
      //// // console.log('📡 Config response status:', response.status);
      
      if (!response.ok) {
        console.warn('Could not load system config for domains, returning empty list');
        return [
          { value: '1', label: 'No domains configured - Configure domains in Network settings first' }
        ];
      }
      
      const configData = await response.json();
      //// // console.log('📄 Full config data:', configData);
      //// // console.log('🔧 Config keys available:', Object.keys(configData));
      
      const config = configData.config || {};
      //// // console.log('⚙️ Config object:', config);
      //// // console.log('🔑 Config keys:', Object.keys(config));
      
      const domains = [];
      
      // Check CFG_DOMAIN_1 through CFG_DOMAIN_9
      for (let i = 1; i <= 9; i++) {
        const domainKey = `CFG_DOMAIN_${i}`;
        const domainConfig = config[domainKey];
        
        //// // console.log(`🌐 Checking ${domainKey}:`, domainConfig, 'type:', typeof domainConfig);
        
        // Check if domainConfig has a value property and it's a non-empty string
        let domainValue = '';
        if (domainConfig && typeof domainConfig === 'object' && domainConfig.value) {
          domainValue = domainConfig.value;
        } else if (typeof domainConfig === 'string') {
          domainValue = domainConfig;
        }
        
        //// // console.log(`🔤 Extracted domain value: "${domainValue}" type: ${typeof domainValue}`);
        
        // Only add domains that have actual content (non-empty string)
        if (typeof domainValue === 'string' && domainValue.trim() !== '') {
          //// // console.log(`✅ Adding domain: ${domainValue.trim()}`);
          domains.push({
            number: i,
            domain: domainValue.trim(),
            key: domainKey
          });
        } else {
          //// // console.log(`⏭️ Skipping empty domain ${domainKey}`);
        }
      }
      
      //// // console.log('✅ Found configured domains:', domains);
      
      if (domains.length === 0) {
        //// // console.log('⚠️ No domains found, returning fallback option');
        return [
          { value: '1', label: 'No domains configured - Configure domains in Network settings first' }
        ];
      }
      
      // Create options with just domain names
      const options = domains.map(domain => ({
        value: domain.number.toString(),
        label: domain.domain
      }));
      
      //// // console.log('✅ Generated domain options:', options);
      return options;
      
    } catch (error) {
      console.error('❌ Error fetching domains:', error);
      return [
        { value: '1', label: 'Error loading domains - Check console for details' }
      ];
    }
  },
  // Enhance scrollbar dynamically for tabs-list
  enhanceTabsScrollbar() {
    const tabsList = document.querySelector('.tabs-list');
    if (tabsList) {
      // Check if scrolling is needed
      const isScrollable = tabsList.scrollWidth > tabsList.clientWidth;
      
      if (isScrollable) {
        // Add data attribute for enhanced styling
        tabsList.setAttribute('data-scrollable', 'true');
        //// // console.log('✅ Enhanced tabs scrollbar for scrollable content');
      } else {
        // Remove attribute if not scrollable
        tabsList.removeAttribute('data-scrollable');
        //// // console.log('📝 Tabs list not scrollable, using default styling');
      }
      
      // Monitor for content changes
      const observer = new MutationObserver(() => {
        setTimeout(() => this.enhanceTabsScrollbar(), 100);
      });
      
      observer.observe(tabsList, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'style']
      });
    }
  },
});
