/* ============================================================
   BAN YARO — UI Helpers
   Alle UI-Interaktionen an einem Ort.
   Toast, Modal, Loading, Confirm — einmal gebaut, überall nutzbar.
   ============================================================ */

const UI = (() => {

  // ----------------------------------------------------------
  // PHOSPHOR ICON HELPER — erzeugt SVG-String für Templates
  // ----------------------------------------------------------
  function _svgIcon(name, extraClass = '') {
    return `<svg class="ph-icon${extraClass ? ' ' + extraClass : ''}" aria-hidden="true"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
  }

  // ----------------------------------------------------------
  // TOAST
  // ----------------------------------------------------------
  const toast = (() => {
    const container = () => document.getElementById('toast-container');

    function show(message, type = 'default', duration = 3500) {
      const el = document.createElement('div');
      el.className = `toast${type !== 'default' ? ` toast-${type}` : ''}`;

      const iconName = { success: 'check', danger: 'x', warning: 'warning', info: 'info' }[type];
      el.innerHTML = iconName
        ? `${_svgIcon(iconName)}<span>${message}</span>`
        : `<span>${message}</span>`;

      container().appendChild(el);

      const timer = setTimeout(() => remove(el), duration);
      el.addEventListener('click', () => { clearTimeout(timer); remove(el); });
    }

    function remove(el) {
      el.classList.add('removing');
      const fallback = setTimeout(() => el.remove(), 300);
      el.addEventListener('animationend', () => { clearTimeout(fallback); el.remove(); }, { once: true });
    }

    // callable as UI.toast(msg, type) and UI.toast.success(msg) etc.
    function t(msg, type = 'default', dur) { show(msg, type, dur); }
    t.show     = show;
    t.success  = (msg, dur) => show(msg, 'success', dur);
    t.error    = (msg, dur) => show(msg, 'danger',  dur || 5000);
    t.warning  = (msg, dur) => show(msg, 'warning', dur);
    t.info     = (msg, dur) => show(msg, 'info',    dur);
    return t;
  })();

  // ----------------------------------------------------------
  // MODAL
  // ----------------------------------------------------------
  const modal = (() => {
    let _current = null;

    function open({ title, body, footer, onClose, size } = {}) {
      close(); // vorheriges schließen

      const overlay = document.createElement('div');
      overlay.className = 'modal-overlay' + (size ? ` modal-overlay--${size}` : '');
      overlay.innerHTML = `
        <div class="modal" role="dialog" aria-modal="true">
          <div class="modal-handle"></div>
          ${title ? `
            <div class="modal-header">
              <span class="modal-title">${title}</span>
              <button class="btn btn-ghost btn-icon modal-close-btn" aria-label="Schließen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg></button>
            </div>
          ` : ''}
          <div class="modal-body">${body || ''}</div>
          ${footer ? `<div class="modal-footer">${footer}</div>` : ''}
        </div>
      `;

      overlay.querySelector('.modal-close-btn')?.addEventListener('click', close);

      document.getElementById('modal-container').appendChild(overlay);
      document.documentElement.classList.add('modal-open');
      _current = { overlay, onClose };

      return overlay.querySelector('.modal');
    }

    function close() {
      if (!_current) return;
      const { onClose } = _current;
      onClose?.();
      _current.overlay.remove();
      document.documentElement.classList.remove('modal-open');
      _current = null;
      // iOS Safari setzt den Zoom nach Input-Fokus nicht zurück — Viewport kurz neu setzen
      const meta = document.querySelector('meta[name="viewport"]');
      if (meta) {
        const orig = meta.content;
        meta.content = orig + ',maximum-scale=1';
        requestAnimationFrame(() => { meta.content = orig; });
      }
    }

    // Bestätigungsdialog
    function confirm({ title, message, confirmText = 'OK', cancelText = 'Abbrechen',
                        danger = false } = {}) {
      return new Promise(resolve => {
        const m = open({
          title,
          body: `<p style="color:var(--c-text-secondary)">${message}</p>`,
          footer: `
            <button class="btn btn-secondary" id="modal-cancel">${cancelText}</button>
            <button class="btn ${danger ? 'btn-danger' : 'btn-primary'}" id="modal-confirm">
              ${confirmText}
            </button>
          `,
          onClose: () => resolve(false),
        });
        m.parentElement.querySelector('#modal-cancel').addEventListener('click', () => {
          resolve(false); close();
        });
        m.parentElement.querySelector('#modal-confirm').addEventListener('click', () => {
          resolve(true); close();
        });
      });
    }

    return { open, close, confirm };
  })();

  // ----------------------------------------------------------
  // LOADING STATE für Buttons
  // ----------------------------------------------------------
  function setLoading(btn, loading) {
    if (loading) {
      btn._originalContent = btn.innerHTML;
      btn.innerHTML = '<span class="spinner spinner-sm"></span>';
      btn.disabled = true;
    } else {
      btn.innerHTML = btn._originalContent || btn.innerHTML;
      btn.disabled = false;
    }
  }

  // ----------------------------------------------------------
  // ASYNC BUTTON: Button-Click → Loader → Ergebnis → Toast
  // Verwendung: UI.asyncButton(btn, async () => { await API.something() })
  // ----------------------------------------------------------
  async function asyncButton(btn, fn, { successMsg, errorMsg } = {}) {
    setLoading(btn, true);
    try {
      const result = await fn();
      if (successMsg) toast.success(successMsg);
      return result;
    } catch (err) {
      const msg = errorMsg || err.message || 'Ein Fehler ist aufgetreten.';
      toast.error(msg);
      throw err;
    } finally {
      setLoading(btn, false);
    }
  }

  // ----------------------------------------------------------
  // FORMULAR-HELPER
  // ----------------------------------------------------------
  function formData(form) {
    const data = {};
    new FormData(form).forEach((v, k) => { data[k] = v; });
    return data;
  }

  function setFormError(form, fieldName, message) {
    const field = form.querySelector(`[name="${fieldName}"]`);
    if (!field) return;
    field.classList.add('is-invalid');
    let hint = field.parentElement.querySelector('.form-error');
    if (!hint) {
      hint = document.createElement('span');
      hint.className = 'form-error';
      field.parentElement.appendChild(hint);
    }
    hint.textContent = message;
  }

  function clearFormErrors(form) {
    form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
    form.querySelectorAll('.form-error').forEach(el => el.remove());
  }

  // ----------------------------------------------------------
  // LEERER ZUSTAND (Empty State)
  // ----------------------------------------------------------
  function emptyState({ icon, title, text, action } = {}) {
    return `
      <div class="empty-state">
        ${icon ? `<div class="empty-state-icon">${icon}</div>` : ''}
        ${title ? `<div class="empty-state-title">${title}</div>` : ''}
        ${text  ? `<div class="empty-state-text">${text}</div>` : ''}
        ${action ? `<div style="margin-top:var(--space-4)">${action}</div>` : ''}
      </div>
    `;
  }

  // ----------------------------------------------------------
  // DATUM-FORMATIERUNG (Deutsch, relativ)
  // ----------------------------------------------------------
  const time = (() => {
    const fmt = new Intl.RelativeTimeFormat('de', { numeric: 'auto' });
    const fmtDate = new Intl.DateTimeFormat('de-DE', {
      day: 'numeric', month: 'long', year: 'numeric'
    });
    const fmtDateShort = new Intl.DateTimeFormat('de-DE', {
      day: 'numeric', month: 'short'
    });

    function relative(dateStr) {
      const diff = (new Date(dateStr) - Date.now()) / 1000;
      const abs  = Math.abs(diff);
      if (abs < 60)   return fmt.format(Math.round(diff), 'second');
      if (abs < 3600) return fmt.format(Math.round(diff / 60), 'minute');
      if (abs < 86400)return fmt.format(Math.round(diff / 3600), 'hour');
      if (abs < 604800) return fmt.format(Math.round(diff / 86400), 'day');
      return fmtDate.format(new Date(dateStr));
    }

    return {
      relative,
      format:      d => fmtDate.format(new Date(d)),
      formatShort: d => fmtDateShort.format(new Date(d)),
    };
  })();

  // ----------------------------------------------------------
  // FOTO-VORSCHAU (Input[type=file] → img)
  // ----------------------------------------------------------
  function setupPhotoPreview(input, imgEl) {
    input.addEventListener('change', () => {
      const file = input.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = e => { imgEl.src = e.target.result; };
      reader.readAsDataURL(file);
    });
  }

  // ----------------------------------------------------------
  // SCROLL TO TOP der Seite
  // ----------------------------------------------------------
  function scrollTop() {
    document.getElementById('page-content')?.scrollTo({ top: 0, behavior: 'smooth' });
  }

  // ----------------------------------------------------------
  // SKELETON LOADER (Platzhalter während Laden)
  // ----------------------------------------------------------
  function skeleton(lines = 3) {
    return Array.from({ length: lines }, (_, i) => `
      <div style="height:${i === 0 ? 20 : 14}px; width:${70 + Math.random() * 30}%;
           background:var(--c-surface-2); border-radius:var(--radius-sm);
           margin-bottom:var(--space-2); animation:skeleton-pulse 1.5s ease infinite">
      </div>
    `).join('') + `
      <style>
        @keyframes skeleton-pulse {
          0%,100% { opacity:1 } 50% { opacity:0.4 }
        }
      </style>
    `;
  }

  function escape(str) {
    if (!str) return '';
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  // Alias für ältere Aufrufe
  const escHtml = escape;

  // ----------------------------------------------------------
  // PAGE INFO — generische Seiten-Hilfe
  //   config: { pageId, title, icon?, intro, steps?: [{icon,title,text}], tip? }
  //   Erstes Öffnen: expandierter Banner. Danach: kleines ? im Header.
  // ----------------------------------------------------------
  function pageInfo(container, config) {
    const seenKey = 'help_seen_' + config.pageId;
    const seen = !!localStorage.getItem(seenKey);

    function _buildSteps() {
      if (!config.steps?.length) return '';
      return config.steps.map(s => `
        <div class="pinfo-step">
          ${s.icon ? `<span class="pinfo-step-icon">${_svgIcon(s.icon)}</span>` : ''}
          <div>
            ${s.title ? `<div class="pinfo-step-title">${s.title}</div>` : ''}
            <div class="pinfo-step-text">${s.text}</div>
          </div>
        </div>`).join('');
    }

    function _openModal() {
      modal.open({
        title: `${_svgIcon(config.icon || 'question')} ${config.title}`,
        body: `
          <div class="pinfo-modal">
            <p class="pinfo-intro">${config.intro}</p>
            ${config.steps?.length ? `<div class="pinfo-steps">${_buildSteps()}</div>` : ''}
            ${config.tip ? `<div class="pinfo-tip">${_svgIcon('lightbulb')} ${config.tip}</div>` : ''}
          </div>`,
      });
    }

    // Kleiner ? Button oben rechts — immer sichtbar
    const headerBtn = document.createElement('button');
    headerBtn.className = 'pinfo-trigger';
    headerBtn.setAttribute('aria-label', 'Hilfe');
    headerBtn.innerHTML = _svgIcon('question');
    headerBtn.addEventListener('click', _openModal);
    container.appendChild(headerBtn);

    // Banner beim ersten Besuch
    if (!seen) {
      localStorage.setItem(seenKey, '1');
      const banner = document.createElement('div');
      banner.className = 'pinfo-banner';
      banner.innerHTML = `
        <div class="pinfo-banner-head">
          <span class="pinfo-banner-icon">${_svgIcon(config.icon || 'info')}</span>
          <span class="pinfo-banner-title">${config.title}</span>
          <button class="pinfo-banner-close" aria-label="Schließen">${_svgIcon('x')}</button>
        </div>
        <div class="pinfo-banner-intro">${config.intro}</div>
        ${config.steps?.length ? `<div class="pinfo-steps pinfo-steps--compact">${_buildSteps()}</div>` : ''}
        <button class="pinfo-banner-more">Mehr erfahren ${_svgIcon('arrow-right')}</button>
      `;
      banner.querySelector('.pinfo-banner-close').addEventListener('click', () => banner.remove());
      banner.querySelector('.pinfo-banner-more').addEventListener('click', () => { banner.remove(); _openModal(); });
      container.insertAdjacentElement('afterbegin', banner);
    }
  }

  // ----------------------------------------------------------
  // HELP TOOLTIP — inline ? Badge mit Klick-Tooltip
  // ----------------------------------------------------------
  function help(text) {
    return `<button class="by-help-btn" data-help="${escape(text)}" aria-label="Hilfe" type="button">
      <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px">
        <use href="/icons/phosphor.svg#question"></use>
      </svg>
    </button>`;
  }

  // Event-Delegation für Help-Tooltips — einmalig registrieren
  document.addEventListener('click', e => {
    const btn = e.target.closest('.by-help-btn');
    if (!btn) {
      document.querySelectorAll('.by-help-tooltip').forEach(t => t.remove());
      return;
    }
    e.stopPropagation();
    document.querySelectorAll('.by-help-tooltip').forEach(t => t.remove());
    const tip = document.createElement('div');
    tip.className = 'by-help-tooltip';
    tip.textContent = btn.dataset.help;
    document.body.appendChild(tip);
    const r = btn.getBoundingClientRect();
    tip.style.top  = (r.bottom + window.scrollY + 6) + 'px';
    tip.style.left = Math.max(8, r.left + window.scrollX - tip.offsetWidth / 2 + r.width / 2) + 'px';
    const maxL = window.innerWidth - tip.offsetWidth - 8;
    if (parseFloat(tip.style.left) > maxL) tip.style.left = maxL + 'px';
  });

  // ----------------------------------------------------------
  // SAVE TO ALBUM — Foto/Video nach Kamera-Aufnahme in Mediathek
  // anbieten. Nur bei capture-Aufnahmen aufrufen (nicht Galerie).
  // Nutzt Web Share API wenn verfügbar, sonst <a download> Fallback.
  // ----------------------------------------------------------
  async function saveToAlbum(file) {
    if (!file) return;

    // Web Share API mit Datei-Support (iOS Safari 15+, Chrome Android)
    if (navigator.canShare && navigator.canShare({ files: [file] })) {
      try {
        await navigator.share({
          files: [file],
          title: file.name || 'Foto',
        });
      } catch (err) {
        // AbortError = User hat Sheet geschlossen — kein Fehler anzeigen
        if (err?.name !== 'AbortError') {
          console.warn('saveToAlbum share error:', err);
        }
      }
      return;
    }

    // Fallback: direkter Download per <a download>
    const url = URL.createObjectURL(file);
    const a   = document.createElement('a');
    a.href     = url;
    a.download = file.name || ('foto_' + Date.now() + (file.type === 'video/mp4' ? '.mp4' : '.jpg'));
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 2000);
  }

  // ----------------------------------------------------------
  // LEAFLET LAZY LOADER — zentrales Laden von Leaflet + MarkerCluster
  // Dedupliziert: mehrere gleichzeitige Aufrufe warten auf dasselbe Promise.
  //
  // Verwendung:
  //   await UI.loadLeaflet();        // nur Leaflet
  //   await UI.loadLeaflet(true);    // Leaflet + MarkerCluster
  // ----------------------------------------------------------
  let _leafletPromise = null;

  function loadLeaflet(withCluster = false) {
    if (!_leafletPromise) {
      _leafletPromise = new Promise((resolve, reject) => {
        // CSS (Duplikat-Check)
        const cssLoaded = document.querySelector('link[href*="leaflet"]')
          ? Promise.resolve()
          : new Promise(res => {
              const link = document.createElement('link');
              link.rel = 'stylesheet'; link.href = '/css/leaflet.css';
              link.onload = res; link.onerror = res;
              document.head.appendChild(link);
            });

        cssLoaded.then(() => {
          if (window.L) { resolve(); return; }
          if (document.querySelector('script[src*="leaflet.js"]')) {
            // Script-Tag schon da — warten bis window.L gesetzt ist
            const poll = setInterval(() => {
              if (window.L) { clearInterval(poll); resolve(); }
            }, 50);
            return;
          }
          const s = document.createElement('script');
          s.src = '/js/leaflet.js';
          s.onload = resolve;
          s.onerror = reject;
          document.head.appendChild(s);
        });
      });
    }

    if (!withCluster) return _leafletPromise;

    // MarkerCluster zusätzlich laden
    return _leafletPromise.then(() => {
      if (window.L && L.markerClusterGroup) return;
      // CSS
      if (!document.querySelector('link[href*="MarkerCluster"]')) {
        ['MarkerCluster.css', 'MarkerCluster.Default.css'].forEach(name => {
          const link = document.createElement('link');
          link.rel = 'stylesheet'; link.href = `/css/${name}`;
          document.head.appendChild(link);
        });
      }
      // JS
      if (document.querySelector('script[src*="markercluster"]') ||
          document.querySelector('script[src*="MarkerCluster"]')) return;
      return new Promise(resolve => {
        const s = document.createElement('script');
        s.src = '/js/leaflet.markercluster.js';
        s.onload  = resolve;
        s.onerror = resolve; // graceful degradation
        document.head.appendChild(s);
      });
    });
  }

  // ----------------------------------------------------------
  // LEAFLET MARKER FACTORY — erzeugt einen L.divIcon-Marker
  // Verwendung:
  //   UI.leafletMarker({ lat, lon, color, icon, size, zIndex })
  //   Gibt ein L.marker-Objekt zurück, das in eine Karte eingefügt werden kann.
  //
  // Params:
  //   color  — CSS-Farbe (z.B. 'var(--c-primary)' oder '#22C55E')
  //   icon   — HTML-String für das Icon (z.B. UI.icon('dog'))
  //   size   — Durchmesser des Kreises in px (default: 32)
  //   label  — optionaler Text der im Kreis angezeigt wird
  // ----------------------------------------------------------
  function leafletMarker({ lat, lon, color = 'var(--c-primary)', icon = '', size = 32, label = '' } = {}) {
    const inner = label || icon;
    const divIcon = L.divIcon({
      className: '',
      html: `<div style="background:${color};color:#fff;font-size:${Math.round(size * 0.45)}px;font-weight:700;width:${size}px;height:${size}px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,0.3);border:2px solid rgba(255,255,255,0.8)">${inner}</div>`,
      iconSize:   [size, size],
      iconAnchor: [size / 2, size / 2],
    });
    return L.marker([lat, lon], { icon: divIcon });
  }

  // ----------------------------------------------------------
  // LOCATION PICKER — zentrale Karten-Komponente
  // Rendert Leaflet-Karte + GPS-Button + Ort-Chip in das Element
  // mit der angegebenen containerId.
  //
  // Verwendung:
  //   const picker = UI.locationPicker({
  //     containerId: 'my-map-wrap',
  //     onSelect(lat, lon, name) { ... }
  //   });
  //   picker.setValue(lat, lon, name);  // vorhandene Werte laden
  //   picker.getValue();                // → { lat, lon, name }
  // ----------------------------------------------------------
  function locationPicker({ containerId, onSelect } = {}) {
    // Interne State-Variablen
    let _lat  = null;
    let _lon  = null;
    let _name = null;
    let _map  = null;
    let _marker = null;

    const _pinSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="36" viewBox="0 0 32 40"><path d="M16 0C7.163 0 0 7.163 0 16c0 10 16 24 16 24S32 26 32 16C32 7.163 24.837 0 16 0z" fill="#C4843A"/><circle cx="16" cy="16" r="7" fill="white"/></svg>';

    function _sourceIcon(source) {
      if (source === 'places') return 'star';
      if (source === 'osm')    return 'map-pin';
      return 'map-trifold';
    }

    // IDs werden mit containerId geprefixt um Konflikte zu vermeiden
    const p = containerId.replace(/[^a-z0-9]/gi, '-');
    const ids = {
      mapWrap:      `${p}-map`,
      chip:         `${p}-chip-wrap`,
      chipLabel:    `${p}-chip-label`,
      chipClear:    `${p}-chip-clear`,
      locBtn:       `${p}-loc-btn`,
      locBtnLabel:  `${p}-loc-btn-label`,
      coordsClear:  `${p}-coords-clear`,
      suggestions:  `${p}-suggestions`,
      pinHere:      `${p}-pin-here`,
    };

    // HTML in den Container rendern
    function _render(container) {
      container.innerHTML = `
        <div style="position:relative">
          <div id="${ids.mapWrap}" style="border-radius:var(--radius-md);overflow:hidden;height:200px;background:var(--c-surface-2)"></div>
          <button type="button" id="${ids.pinHere}" style="
            position:absolute;bottom:10px;left:50%;transform:translateX(-50%);
            z-index:1000;background:var(--c-primary);color:#fff;border:none;
            border-radius:var(--radius-full);padding:6px 14px;font-size:var(--text-xs);
            font-weight:600;box-shadow:var(--shadow-md);cursor:pointer;
            display:flex;align-items:center;gap:6px;white-space:nowrap">
            ${_svgIcon('map-pin')} Pin hier setzen
          </button>
        </div>
        <div style="margin-top:var(--space-2)">
          <div id="${ids.chip}" style="display:none">
            <div class="diary-location-chip">
              ${_svgIcon('map-pin')}
              <span id="${ids.chipLabel}"></span>
              <button type="button" id="${ids.chipClear}" aria-label="Name entfernen">
                ${_svgIcon('x')}
              </button>
            </div>
          </div>
          <div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
            <button type="button" class="btn btn-danger btn-sm" id="${ids.coordsClear}">Ort entfernen</button>
            <button type="button" class="btn btn-secondary flex-1" id="${ids.locBtn}">
              ${_svgIcon('map-pin')}
              <span id="${ids.locBtnLabel}">GPS → POI suchen</span>
            </button>
          </div>
          <div id="${ids.suggestions}" style="display:none;margin-top:var(--space-2)"></div>
        </div>
      `;
    }

    function _getEl(id) { return document.getElementById(id); }

    function _mkIcon() {
      return L.divIcon({ html: _pinSvg, className: '', iconSize: [28, 36], iconAnchor: [14, 36] });
    }

    function _placeMarker(lat, lon) {
      if (_marker) { _marker.setLatLng([lat, lon]); return; }
      _marker = L.marker([lat, lon], { draggable: true, icon: _mkIcon() }).addTo(_map);
      _marker.on('dragend', () => {
        const p2 = _marker.getLatLng();
        _lat = p2.lat; _lon = p2.lng;
        const lbl = _getEl(ids.locBtnLabel);
        if (lbl) lbl.textContent = 'POI suchen';
        onSelect?.(_lat, _lon, _name);
      });
    }

    function _setCoords(lat, lon) {
      _lat = lat; _lon = lon;
    }

    function _setName(name) {
      _name = name;
      const chipLbl = _getEl(ids.chipLabel);
      const chipWrap = _getEl(ids.chip);
      const sugEl   = _getEl(ids.suggestions);
      if (chipLbl)  chipLbl.textContent = name;
      if (chipWrap) chipWrap.style.display = '';
      if (sugEl)    sugEl.style.display = 'none';
      onSelect?.(_lat, _lon, _name);
    }

    function _loadLeafletLocal() {
      if (window.L) return Promise.resolve();
      return new Promise((resolve, reject) => {
        const cssLoaded = document.querySelector('link[href*="leaflet"]')
          ? Promise.resolve()
          : new Promise(res => {
              const link = document.createElement('link');
              link.rel = 'stylesheet'; link.href = '/css/leaflet.css';
              link.onload = res; link.onerror = res;
              document.head.appendChild(link);
            });
        cssLoaded.then(() => {
          if (document.querySelector('script[src*="leaflet.js"]')) { resolve(); return; }
          const s = document.createElement('script');
          s.src = '/js/leaflet.js'; s.onload = resolve; s.onerror = reject;
          document.head.appendChild(s);
        });
      });
    }

    function _initMap() {
      _loadLeafletLocal().then(() => {
        setTimeout(() => {
          const mapEl = _getEl(ids.mapWrap);
          if (!mapEl) return;
          const lat  = _lat  || 48.0;
          const lon  = _lon  || 11.9;
          const zoom = _lat  ? 15 : 7;
          _map = L.map(ids.mapWrap, {
            zoomControl: true, attributionControl: false,
            dragging: true, scrollWheelZoom: false,
          }).setView([lat, lon], zoom);
          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
            .addTo(_map);
          _map.invalidateSize();
          setTimeout(() => _map?.invalidateSize(), 300);
          if (_lat) _placeMarker(lat, lon);

          _map.on('click', e => {
            _setCoords(e.latlng.lat, e.latlng.lng);
            _placeMarker(_lat, _lon);
            const lbl = _getEl(ids.locBtnLabel);
            if (lbl) lbl.textContent = 'POI suchen';
            onSelect?.(_lat, _lon, _name);
          });

          _getEl(ids.pinHere)?.addEventListener('click', () => {
            const c = _map.getCenter();
            _setCoords(c.lat, c.lng);
            _placeMarker(c.lat, c.lng);
            const lbl = _getEl(ids.locBtnLabel);
            if (lbl) lbl.textContent = 'POI suchen';
            onSelect?.(_lat, _lon, _name);
          });
        }, 150);
      });
    }

    function _bindEvents() {
      // Chip-Name entfernen
      _getEl(ids.chipClear)?.addEventListener('click', () => {
        _name = null;
        const chipWrap = _getEl(ids.chip);
        if (chipWrap) chipWrap.style.display = 'none';
        onSelect?.(_lat, _lon, null);
      });

      // Koordinaten + Name komplett entfernen (Zwei-Klick)
      const coordsClearBtn = _getEl(ids.coordsClear);
      let _clearPending = false;
      coordsClearBtn?.addEventListener('click', () => {
        if (!_clearPending) {
          _clearPending = true;
          coordsClearBtn.textContent = 'Wirklich entfernen?';
          coordsClearBtn.style.color = 'var(--c-danger)';
          setTimeout(() => {
            _clearPending = false;
            if (coordsClearBtn) {
              coordsClearBtn.textContent = 'Ort entfernen';
              coordsClearBtn.style.color = '';
            }
          }, 3000);
          return;
        }
        _clearPending = false;
        coordsClearBtn.textContent = 'Ort entfernen';
        coordsClearBtn.style.color = '';
        _lat = null; _lon = null; _name = null;
        const chipWrap = _getEl(ids.chip);
        const sugEl   = _getEl(ids.suggestions);
        const lbl     = _getEl(ids.locBtnLabel);
        if (chipWrap) chipWrap.style.display = 'none';
        if (sugEl)    sugEl.style.display = 'none';
        if (lbl)      lbl.textContent = 'GPS → POI suchen';
        if (_marker)  { _marker.remove(); _marker = null; }
        if (_map)     _map.setView([48.0, 11.9], 7);
        onSelect?.(null, null, null);
      });

      // GPS-Button + POI-Suche
      async function _showSuggestions() {
        const btn = _getEl(ids.locBtn);
        if (btn) setLoading(btn, true);
        try {
          let lat = _lat, lon = _lon;
          if (lat == null || lon == null) {
            const pos = await API.getLocation({ enableHighAccuracy: true });
            lat = pos.lat; lon = pos.lon;
            _setCoords(lat, lon);
            if (_map) {
              _map.setView([lat, lon], 15);
              _placeMarker(lat, lon);
            }
            const lbl = _getEl(ids.locBtnLabel);
            if (lbl) lbl.textContent = 'POI suchen';
          }

          let suggestions = [];
          try {
            suggestions = await API.walks.nearby(lat, lon);
          } catch {}

          const sugEl = _getEl(ids.suggestions);
          if (!sugEl) return;
          if (!suggestions.length) {
            sugEl.innerHTML = '<p style="font-size:var(--text-sm);color:var(--c-text-secondary);padding:var(--space-2) 0">Keine Orte in der Nähe gefunden.</p>';
          } else {
            sugEl.innerHTML = suggestions.map(s => `
              <button type="button" class="diary-location-suggestion"
                      data-name="${escape(s.name)}" data-lat="${s.lat}" data-lon="${s.lon}">
                ${_svgIcon(_sourceIcon(s.source))}
                <span>${escape(s.name)}</span>
                <small>${s.distance_m < 1000 ? s.distance_m + ' m' : (s.distance_m / 1000).toFixed(1) + ' km'}</small>
              </button>`).join('');
            sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => {
              el.addEventListener('click', () => {
                const slat = parseFloat(el.dataset.lat);
                const slon = parseFloat(el.dataset.lon);
                _setCoords(slat, slon);
                _setName(el.dataset.name);
                if (_map) {
                  _map.setView([slat, slon], 16);
                  _placeMarker(slat, slon);
                }
              });
            });
          }
          sugEl.style.display = '';
          onSelect?.(_lat, _lon, _name);
        } catch (err) {
          toast.error(err?.message?.includes('GPS') || _lat == null
            ? 'GPS nicht verfügbar.' : 'Ortssuche fehlgeschlagen.');
        } finally {
          if (btn) setLoading(btn, false);
        }
      }

      _getEl(ids.locBtn)?.addEventListener('click', _showSuggestions);
    }

    // Container initialisieren
    const container = document.getElementById(containerId);
    if (!container) {
      console.warn('UI.locationPicker: containerId nicht gefunden:', containerId);
      return { getValue: () => ({ lat: null, lon: null, name: null }), setValue: () => {} };
    }

    _render(container);
    _bindEvents();
    _initMap();

    // Öffentliche API des Pickers
    return {
      getValue() {
        return { lat: _lat, lon: _lon, name: _name };
      },
      setValue(lat, lon, name) {
        _lat  = lat  != null ? parseFloat(lat)  : null;
        _lon  = lon  != null ? parseFloat(lon)  : null;
        _name = name || null;
        // Chip aktualisieren
        const chipLbl  = _getEl(ids.chipLabel);
        const chipWrap = _getEl(ids.chip);
        const lbl      = _getEl(ids.locBtnLabel);
        if (chipLbl)  chipLbl.textContent = _name || '';
        if (chipWrap) chipWrap.style.display = _name ? '' : 'none';
        if (lbl)      lbl.textContent = _lat ? 'POI suchen' : 'GPS → POI suchen';
        // Karte anpassen wenn bereits initialisiert
        if (_map && _lat) {
          _map.setView([_lat, _lon], 15);
          _placeMarker(_lat, _lon);
        }
      },
    };
  }

  // ----------------------------------------------------------
  // RATING STARS — wiederverwendbare Bewertungskomponente
  // Verwendung: UI.ratingStars({ containerId, targetType, targetId, isLoggedIn })
  // Rendert Sterne-Anzeige + Inline-Widget zum Bewerten
  // ----------------------------------------------------------
  function ratingStars({ containerId, targetType, targetId, isLoggedIn }) {
    const container = document.getElementById(containerId);
    if (!container) return;

    let _avgStars     = 0;
    let _anzahl       = 0;
    let _myStars      = null;
    let _myKommentar  = '';
    let _hoverStar    = 0;
    let _widgetOpen   = false;

    function _starHTML(filled, half = false, idx = 0) {
      const cls = filled ? 'rating-star rating-star--filled' : (half ? 'rating-star rating-star--half' : 'rating-star rating-star--empty');
      return `<span class="${cls}" data-star="${idx}" aria-label="${idx} Sterne">★</span>`;
    }

    function _renderAvg() {
      const stars = [];
      for (let i = 1; i <= 5; i++) {
        const diff = _avgStars - (i - 1);
        if (diff >= 1)        stars.push(_starHTML(true, false, i));
        else if (diff >= 0.4) stars.push(_starHTML(false, true, i));
        else                  stars.push(_starHTML(false, false, i));
      }
      return stars.join('');
    }

    function _renderWidget() {
      const stars = [];
      for (let i = 1; i <= 5; i++) {
        const active = (_hoverStar || _myStars || 0) >= i;
        stars.push(`<span class="rating-star rating-star--pick${active ? ' rating-star--filled' : ''}" data-pick="${i}" aria-label="${i} Sterne">★</span>`);
      }
      return `
        <div class="rating-widget" id="rw-${containerId}">
          <div class="rating-pick-stars">${stars.join('')}</div>
          <textarea class="form-control rating-kommentar" id="rw-komm-${containerId}"
                    placeholder="Kurzer Kommentar (optional, max. 200 Zeichen)"
                    maxlength="200" rows="2">${_myKommentar || ''}</textarea>
          <div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
            <button class="btn btn-secondary btn-sm" id="rw-cancel-${containerId}">Abbrechen</button>
            <button class="btn btn-primary btn-sm" id="rw-save-${containerId}"
                    ${!(_hoverStar || _myStars) ? 'disabled' : ''}>Speichern</button>
          </div>
        </div>
      `;
    }

    function _render() {
      const avgLabel = _anzahl > 0
        ? `${_avgStars.toFixed(1)} (${_anzahl} Bewertung${_anzahl !== 1 ? 'en' : ''})`
        : 'Noch keine Bewertungen';

      const rateHint = isLoggedIn
        ? `<button class="btn btn-ghost btn-sm rating-rate-btn" id="rw-open-${containerId}" style="font-size:var(--text-sm)">
            ${_myStars ? '★ Bewertung ändern' : '★ Bewerten'}
           </button>`
        : '';

      container.innerHTML = `
        <div class="rating-display">
          <div class="rating-stars-avg">${_renderAvg()}</div>
          <span class="rating-avg-label">${avgLabel}</span>
          ${rateHint}
        </div>
        ${_widgetOpen ? _renderWidget() : ''}
      `;

      // Events
      document.getElementById(`rw-open-${containerId}`)?.addEventListener('click', () => {
        _widgetOpen = true;
        _render();
        _bindWidget();
      });
    }

    function _bindWidget() {
      const widget = document.getElementById(`rw-${containerId}`);
      if (!widget) return;

      // Hover
      widget.querySelectorAll('[data-pick]').forEach(el => {
        el.addEventListener('mouseenter', () => {
          _hoverStar = parseInt(el.dataset.pick);
          _render();
          _bindWidget();
        });
        el.addEventListener('mouseleave', () => {
          _hoverStar = 0;
          _render();
          _bindWidget();
        });
        el.addEventListener('click', () => {
          _myStars = parseInt(el.dataset.pick);
          _hoverStar = 0;
          _render();
          _bindWidget();
          const saveBtn = document.getElementById(`rw-save-${containerId}`);
          if (saveBtn) saveBtn.disabled = false;
        });
        // Touch
        el.addEventListener('touchend', (e) => {
          e.preventDefault();
          _myStars = parseInt(el.dataset.pick);
          _hoverStar = 0;
          _render();
          _bindWidget();
          const saveBtn = document.getElementById(`rw-save-${containerId}`);
          if (saveBtn) saveBtn.disabled = false;
        });
      });

      document.getElementById(`rw-cancel-${containerId}`)?.addEventListener('click', () => {
        _widgetOpen = false;
        _hoverStar  = 0;
        _render();
      });

      document.getElementById(`rw-save-${containerId}`)?.addEventListener('click', async () => {
        if (!_myStars) return;
        const saveBtn = document.getElementById(`rw-save-${containerId}`);
        if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '…'; }
        const komm = document.getElementById(`rw-komm-${containerId}`)?.value?.trim() || null;
        try {
          const res = await API.ratings.rate(targetType, targetId, _myStars, komm);
          _avgStars      = res.bewertung;
          _anzahl        = res.anz_bewertungen;
          _myKommentar   = komm || '';
          _widgetOpen    = false;
          _hoverStar     = 0;
          _render();
          toast.success('Bewertung gespeichert!');
        } catch (err) {
          toast.error(err?.message || 'Fehler beim Speichern.');
          if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
        }
      });
    }

    async function _load() {
      try {
        const [overview, mine] = await Promise.all([
          API.ratings.list(targetType, targetId),
          isLoggedIn ? API.ratings.mine(targetType, targetId) : Promise.resolve({ stars: null, kommentar: null }),
        ]);
        _avgStars    = overview.bewertung || 0;
        _anzahl      = overview.anz_bewertungen || 0;
        _myStars     = mine.stars || null;
        _myKommentar = mine.kommentar || '';
      } catch (e) {
        // silent – Bewertungen sind optional
      }
      _render();
    }

    _load();
  }

  // Öffentliche API
  return {
    toast, modal,
    setLoading, asyncButton,
    formData, setFormError, clearFormErrors,
    emptyState, time,
    setupPhotoPreview, scrollTop, skeleton,
    icon: _svgIcon,
    escape, escHtml, help, pageInfo,
    saveToAlbum,
    loadLeaflet,
    leafletMarker,
    locationPicker,
    ratingStars,
  };

})();
