/* Custom <select> enhancer.
 *
 * Native <select> popups are drawn by the OS and largely ignore CSS,
 * which is why dropdowns rendered like "Local / Git Repository" or
 * "TLS / SSL / None" appear with stock white chrome on Linux/Firefox
 * even with our themed select.form-control rule. This module enhances
 * every <select class="form-control"> by:
 *
 *   1. Keeping the native <select> in the DOM so form submission and
 *      any existing code reading .value / dispatching change events
 *      keeps working — it's just visually replaced.
 *   2. Building a sibling <button> + popup <div> that we fully theme.
 *   3. Wiring keyboard nav (ArrowUp/Down, Enter, Escape, Home/End)
 *      and ARIA roles for screen readers.
 *   4. Watching the DOM (MutationObserver) so dynamically rendered
 *      forms (apps-manager, config-renderer, etc.) auto-enhance.
 *
 * Opt-out: put data-no-enhance on the <select> to keep the native one.
 */

(() => {
  const ENHANCED = Symbol('customSelectEnhanced');

  class CustomSelect {
    constructor(selectEl) {
      if (selectEl[ENHANCED]) return;
      selectEl[ENHANCED] = true;
      this.select = selectEl;
      this.build();
      this.bind();
      this.sync();
    }

    build() {
      this.wrapper = document.createElement('div');
      this.wrapper.className = 'custom-select';
      if (this.select.disabled) this.wrapper.classList.add('is-disabled');

      this.button = document.createElement('button');
      this.button.type = 'button';
      this.button.className = 'custom-select-button';
      this.button.setAttribute('aria-haspopup', 'listbox');
      this.button.setAttribute('aria-expanded', 'false');
      if (this.select.disabled) this.button.disabled = true;

      this.label = document.createElement('span');
      this.label.className = 'custom-select-label';

      this.arrow = document.createElement('span');
      this.arrow.className = 'custom-select-arrow';
      this.arrow.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';

      this.button.append(this.label, this.arrow);

      this.popup = document.createElement('div');
      this.popup.className = 'custom-select-popup';
      this.popup.setAttribute('role', 'listbox');
      this.popup.hidden = true;
      // The popup is portaled to <body> on open (below), escaping .topbar-controls,
      // so carry the topbar's wider-popup styling via a class instead.
      if (this.select.closest('.topbar-controls')) {
        this.popup.classList.add('custom-select-popup-topbar');
      }

      // Move the native select inside the wrapper so it stays in the form. The
      // popup is deliberately NOT added here — it's portaled to <body> on
      // open() so no ancestor's transform (e.g. a card's :hover translateY,
      // which becomes the containing block for position:fixed) can throw off
      // its placement or clip it. See open()/close().
      this.select.parentNode.insertBefore(this.wrapper, this.select);
      this.wrapper.append(this.select, this.button);
      this.select.classList.add('custom-select-native');
      // Forward width-related layout from the native select to the wrapper.
      if (this.select.style.maxWidth) {
        this.wrapper.style.maxWidth = this.select.style.maxWidth;
      }
      if (this.select.style.width && this.select.style.width !== '100%') {
        this.wrapper.style.width = this.select.style.width;
      }
    }

    sync() {
      this.renderOptions();
      this.renderLabel();
    }

    renderOptions() {
      this.popup.innerHTML = '';
      const opts = [...this.select.options];
      opts.forEach((opt, idx) => {
        const li = document.createElement('div');
        li.className = 'custom-select-option';
        li.setAttribute('role', 'option');
        li.dataset.value = opt.value;
        li.dataset.index = String(idx);
        li.textContent = opt.textContent;
        if (opt.disabled) li.classList.add('is-disabled');
        if (opt.selected) {
          li.classList.add('is-selected');
          li.setAttribute('aria-selected', 'true');
        }
        this.popup.appendChild(li);
      });
    }

    renderLabel() {
      const sel = this.select.options[this.select.selectedIndex];
      this.label.textContent = sel ? sel.textContent : '';
    }

    open() {
      if (this.select.disabled) return;
      // Close any other open popup first.
      document.querySelectorAll('.custom-select.is-open').forEach(w => {
        if (w !== this.wrapper) w.dispatchEvent(new CustomEvent('custom-select:close'));
      });
      // Portal into <body> so position:fixed is relative to the viewport,
      // immune to any transformed/overflow ancestor.
      document.body.appendChild(this.popup);
      this.popup.hidden = false;
      this.button.setAttribute('aria-expanded', 'true');
      this.wrapper.classList.add('is-open');
      this.positionPopup();
      // Initial focus highlight on the currently-selected option.
      const sel = this.popup.querySelector('.is-selected');
      if (sel) {
        this.setFocused(sel);
        sel.scrollIntoView({ block: 'nearest' });
      }
      document.addEventListener('click', this._outsideClick, true);
      document.addEventListener('keydown', this._onKey);
      window.addEventListener('resize', this._reposition);
      window.addEventListener('scroll', this._reposition, true);
    }

    close() {
      if (this.popup.hidden) return;
      this.popup.hidden = true;
      this.popup.remove(); // detach from <body> so popups don't accumulate
      this.button.setAttribute('aria-expanded', 'false');
      this.wrapper.classList.remove('is-open');
      this.clearFocused();
      document.removeEventListener('click', this._outsideClick, true);
      document.removeEventListener('keydown', this._onKey);
      window.removeEventListener('resize', this._reposition);
      window.removeEventListener('scroll', this._reposition, true);
    }

    positionPopup() {
      // Popup is position: fixed so it can escape any ancestor with
      // overflow: hidden (modals, .container, etc.). We compute the
      // coordinates here from the button's viewport rect.
      const rect = this.button.getBoundingClientRect();
      const spaceBelow = window.innerHeight - rect.bottom;
      const popupMax = 280;
      const flipUp = spaceBelow < 200 && rect.top > popupMax;
      this.popup.classList.toggle('flip-up', flipUp);

      this.popup.style.left = `${rect.left}px`;
      this.popup.style.width = `${rect.width}px`;
      if (flipUp) {
        this.popup.style.top = 'auto';
        this.popup.style.bottom = `${window.innerHeight - rect.top + 6}px`;
      } else {
        this.popup.style.top = `${rect.bottom + 6}px`;
        this.popup.style.bottom = 'auto';
      }
    }

    setFocused(el) {
      this.clearFocused();
      if (!el) return;
      el.classList.add('is-focused');
    }

    clearFocused() {
      this.popup.querySelectorAll('.is-focused').forEach(el => el.classList.remove('is-focused'));
    }

    pick(value) {
      // Don't dispatch if value unchanged.
      if (this.select.value !== value) {
        this.select.value = value;
        // Native change event so existing onchange handlers run.
        this.select.dispatchEvent(new Event('change', { bubbles: true }));
        this.select.dispatchEvent(new Event('input', { bubbles: true }));
      }
      this.sync();
      this.close();
      this.button.focus();
    }

    bind() {
      this._onKey = (e) => {
        const opts = [...this.popup.querySelectorAll('.custom-select-option:not(.is-disabled)')];
        if (!opts.length) return;
        const focused = this.popup.querySelector('.is-focused');
        const i = focused ? opts.indexOf(focused) : opts.findIndex(o => o.classList.contains('is-selected'));
        switch (e.key) {
          case 'Escape':
            e.preventDefault();
            this.close();
            this.button.focus();
            return;
          case 'Enter':
          case ' ':
            e.preventDefault();
            if (focused) this.pick(focused.dataset.value);
            return;
          case 'ArrowDown': {
            e.preventDefault();
            const next = opts[Math.min(i + 1, opts.length - 1)] ?? opts[0];
            this.setFocused(next);
            next.scrollIntoView({ block: 'nearest' });
            return;
          }
          case 'ArrowUp': {
            e.preventDefault();
            const prev = opts[Math.max(i - 1, 0)] ?? opts[opts.length - 1];
            this.setFocused(prev);
            prev.scrollIntoView({ block: 'nearest' });
            return;
          }
          case 'Home':
            e.preventDefault();
            this.setFocused(opts[0]);
            opts[0].scrollIntoView({ block: 'nearest' });
            return;
          case 'End':
            e.preventDefault();
            this.setFocused(opts[opts.length - 1]);
            opts[opts.length - 1].scrollIntoView({ block: 'nearest' });
            return;
        }
      };

      this._outsideClick = (e) => {
        if (!this.wrapper.contains(e.target) && !this.popup.contains(e.target)) this.close();
      };

      this._reposition = () => this.positionPopup();

      this.button.addEventListener('click', (e) => {
        e.preventDefault();
        if (this.wrapper.classList.contains('is-open')) this.close();
        else this.open();
      });

      this.button.addEventListener('keydown', (e) => {
        if (this.wrapper.classList.contains('is-open')) return;
        if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowUp') {
          e.preventDefault();
          this.open();
        }
      });

      this.popup.addEventListener('click', (e) => {
        const opt = e.target.closest('.custom-select-option');
        if (!opt || opt.classList.contains('is-disabled')) return;
        e.preventDefault();
        e.stopPropagation();
        this.pick(opt.dataset.value);
      });

      this.popup.addEventListener('mouseover', (e) => {
        const opt = e.target.closest('.custom-select-option');
        if (opt && !opt.classList.contains('is-disabled')) this.setFocused(opt);
      });

      this.wrapper.addEventListener('custom-select:close', () => this.close());

      // External code may set select.value programmatically — listen for
      // the change event so the visual stays in sync.
      this.select.addEventListener('change', () => this.sync());

      // Watch for options being replaced (e.g. config-options.js repaints
      // the VPN-type select when provider changes).
      this.optionsObserver = new MutationObserver(() => this.sync());
      this.optionsObserver.observe(this.select, { childList: true, attributes: true, attributeFilter: ['disabled'] });
    }
  }

  // Classes that participate in the themed dropdown. Add more here
  // (or use data-no-enhance to opt a specific <select> out) as new
  // dropdown surfaces appear in the app.
  const ENHANCE_CLASSES = ['form-control', 'theme-selector'];
  const ENHANCE_SELECTOR = ENHANCE_CLASSES.map(c => `select.${c}`).join(',');

  function shouldEnhance(el) {
    if (!(el instanceof HTMLSelectElement)) return false;
    if (el[ENHANCED]) return false;
    if (el.multiple) return false;                 // multi-selects need different UX
    if (el.hasAttribute('data-no-enhance')) return false;
    return ENHANCE_CLASSES.some(c => el.classList.contains(c));
  }

  function enhanceAll(root = document) {
    root.querySelectorAll(ENHANCE_SELECTOR).forEach(s => {
      if (shouldEnhance(s)) new CustomSelect(s);
    });
  }

  function start() {
    enhanceAll();
    // Catch dynamically rendered forms (apps-manager, config-renderer, etc.).
    const observer = new MutationObserver((mutations) => {
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (node.nodeType !== 1) continue;
          if (shouldEnhance(node)) {
            new CustomSelect(node);
          } else {
            enhanceAll(node);
          }
        }
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  // This script is loaded from <head>, so document.body may not exist
  // yet at execution time. Wait for DOMContentLoaded before touching it.
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start);
  } else {
    start();
  }

  // Public hook in case a screen wants to force re-enhance (rare).
  window.CustomSelect = { enhance: enhanceAll, _Class: CustomSelect };
})();
