/* ════════════════════════════════════════════════════════════════════════════
   dashboards-native-mutations.jsx
   Inline mutation flows that replace the "Open in classic ↗" deep-links
   for the most-used create/edit operations. After v81, the host can add
   a villa, the admin can create a host account, and the host can edit
   their settings without ever leaving the new editorial dashboards.

   Pattern:
     • Each wizard is a Modal component portaled to <body>
     • Multi-step wizards render a step indicator + per-step form
     • All POSTs use the SAME endpoints the operational pages call so
       data stays compatible. No schema changes.
     • Loading/error/success states styled with the editorial primitives.

   Exported via window.TLB_NATIVE_MUTATIONS so other section components
   can open them on demand.
   ════════════════════════════════════════════════════════════════════════════
*/

const { useState: useState_M, useEffect: useEffect_M, useRef: useRef_M } = React;

// ── Modal shell ─────────────────────────────────────────────────────────
function Modal({ open, onClose, title, subtitle, children, width = 720 }) {
  useEffect_M(() => {
    if (!open) return;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = '';
      document.removeEventListener('keydown', onKey);
    };
  }, [open, onClose]);

  if (!open) return null;
  return ReactDOM.createPortal(
    <div
      style={{ position: 'fixed', inset: 0, background: 'rgba(20,20,20,0.55)', backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', zIndex: 9000, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: '60px 20px 40px', overflowY: 'auto' }}
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div style={{ background: CC.white, borderRadius: 16, width: '100%', maxWidth: width, boxShadow: '0 24px 80px rgba(0,0,0,0.25)', overflow: 'hidden' }}>
        <div style={{ padding: '24px 28px 18px', borderBottom: `1px solid ${CC.border}`, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
          <div>
            <Display size={24} italic weight={400} style={{ marginBottom: 4 }}>{title}.</Display>
            {subtitle && <Sans size={12} color={CC.mid}>{subtitle}</Sans>}
          </div>
          <button
            onClick={onClose}
            aria-label="Close"
            style={{ width: 32, height: 32, borderRadius: 8, border: `1px solid ${CC.border}`, background: CC.white, cursor: 'pointer', fontSize: 18, color: CC.mid, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
          >×</button>
        </div>
        <div style={{ padding: 28 }}>
          {children}
        </div>
      </div>
    </div>,
    document.body
  );
}

// ── Form atoms ──────────────────────────────────────────────────────────
function Field({ label, hint, children, required }) {
  return (
    <div style={{ marginBottom: 18 }}>
      <label style={{ display: 'block', marginBottom: 8 }}>
        <Sans size={11} weight={600} ls={0.10} upper color={CC.deep}>
          {label}{required && <span style={{ color: '#9a1c1c', marginLeft: 4 }}>*</span>}
        </Sans>
        {hint && <Sans size={11} color={CC.mid} style={{ marginTop: 4, fontWeight: 400, letterSpacing: 0, textTransform: 'none' }}>{hint}</Sans>}
      </label>
      {children}
    </div>
  );
}

const inputStyle = {
  width: '100%',
  padding: '11px 14px',
  border: `1px solid ${CC.border}`,
  borderRadius: 10,
  fontSize: 13,
  fontFamily: F_TLB.ui,
  color: CC.deep,
  outline: 'none',
  background: CC.white,
  transition: 'border-color .15s',
};

function Input(props) {
  return <input {...props} style={{ ...inputStyle, ...(props.style || {}) }} />;
}
function Textarea(props) {
  return <textarea {...props} style={{ ...inputStyle, minHeight: 100, resize: 'vertical', ...(props.style || {}) }} />;
}
function Select({ children, ...props }) {
  return <select {...props} style={{ ...inputStyle, cursor: 'pointer', appearance: 'none', backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'12\' height=\'8\'><path d=\'M1 1l5 5 5-5\' stroke=\'%23999\' fill=\'none\' stroke-width=\'1.5\'/></svg>")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center', paddingRight: 36, ...(props.style || {}) }}>{children}</select>;
}

function PrimaryBtn({ children, ...props }) {
  return (
    <button
      {...props}
      style={{
        padding: '11px 22px', borderRadius: 100, border: 'none',
        background: CC.ink, color: CC.white, fontSize: 12, fontWeight: 600,
        letterSpacing: '0.04em', cursor: props.disabled ? 'not-allowed' : 'pointer',
        opacity: props.disabled ? 0.5 : 1, fontFamily: F_TLB.ui,
        ...(props.style || {}),
      }}
    >
      {children}
    </button>
  );
}

function SecondaryBtn({ children, ...props }) {
  return (
    <button
      {...props}
      style={{
        padding: '10px 20px', borderRadius: 100, border: `1px solid ${CC.border}`,
        background: CC.white, color: CC.deep, fontSize: 12, fontWeight: 500,
        letterSpacing: '0.04em', cursor: 'pointer', fontFamily: F_TLB.ui,
        ...(props.style || {}),
      }}
    >
      {children}
    </button>
  );
}

// ── Step indicator ──────────────────────────────────────────────────────
function StepDots({ current, total }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 24 }}>
      {Array.from({ length: total }, (_, i) => (
        <React.Fragment key={i}>
          <div style={{
            width: 28, height: 28, borderRadius: '50%',
            background: i <= current ? CC.ink : CC.offWhite,
            color: i <= current ? CC.white : CC.mid,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 11, fontWeight: 600, fontFamily: F_TLB.ui,
            border: `1px solid ${i <= current ? CC.ink : CC.border}`,
            transition: 'all .25s',
          }}>{i + 1}</div>
          {i < total - 1 && (
            <div style={{ flex: 1, height: 1, background: i < current ? CC.ink : CC.border, transition: 'background .25s' }} />
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

// ── Toast notification ──────────────────────────────────────────────────
function showToast(msg, tone = 'success') {
  const t = document.createElement('div');
  t.textContent = msg;
  const bg = tone === 'success' ? '#1f6b3a' : tone === 'danger' ? '#9a1c1c' : '#141414';
  t.style.cssText = `position:fixed;bottom:32px;right:32px;background:${bg};color:#fff;padding:14px 22px;border-radius:100px;font-family:'Plus Jakarta Sans',system-ui,sans-serif;font-size:13px;font-weight:500;z-index:10000;box-shadow:0 12px 36px rgba(0,0,0,0.25);animation:tlbToastIn .25s ease-out`;
  document.body.appendChild(t);
  setTimeout(() => { t.style.animation = 'tlbToastOut .25s ease-in forwards'; setTimeout(() => t.remove(), 250); }, 3500);
}
if (!document.getElementById('tlb-toast-keyframes')) {
  const s = document.createElement('style');
  s.id = 'tlb-toast-keyframes';
  s.textContent = `@keyframes tlbToastIn{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes tlbToastOut{to{transform:translateY(20px);opacity:0}}`;
  document.head.appendChild(s);
}

// ════════════════════════════════════════════════════════════════════════
// HOST · ADD PROPERTY WIZARD (3 steps)
// ════════════════════════════════════════════════════════════════════════
// Step 1: Basics — name, location, type, bedrooms, bathrooms, guests
// Step 2: Pricing — nightly rate, currency, min stay
// Step 3: Description + photos — story, hero photo URL, gallery URLs
// On complete: POST /api/properties (host_token auth), close modal,
//              dispatch tlb-villa-created so HostProperties refetches.
function AddPropertyWizard({ open, onClose }) {
  const [step, setStep] = useState_M(0);
  const [busy, setBusy] = useState_M(false);
  const [err, setErr] = useState_M('');
  const [form, setForm] = useState_M({
    name: '', location: 'Seminyak', type: 'villa',
    bedrooms: 3, bathrooms: 3, guests: 6,
    nightly_rate: 5000000, currency: 'IDR', min_nights: 2,
    description: '', image: '', photos_csv: '',
  });

  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
  const setNum = (k) => (e) => setForm({ ...form, [k]: parseInt(e.target.value, 10) || 0 });

  function next() {
    if (step === 0 && !form.name.trim()) { setErr('Villa name is required.'); return; }
    if (step === 1 && (!form.nightly_rate || form.nightly_rate < 100000)) { setErr('Nightly rate must be at least IDR 100,000.'); return; }
    setErr('');
    setStep(s => s + 1);
  }

  function submit() {
    if (!form.description.trim()) { setErr('A description is required (min 50 characters).'); return; }
    if (form.description.length < 50) { setErr('Description should be at least 50 characters for SEO + guest reassurance.'); return; }
    setErr(''); setBusy(true);
    const payload = {
      name: form.name.trim(),
      location: form.location,
      type: form.type,
      bedrooms: Number(form.bedrooms),
      bathrooms: Number(form.bathrooms),
      guests: Number(form.guests),
      nightly_rate: Number(form.nightly_rate),
      currency: form.currency,
      min_nights: Number(form.min_nights),
      description: form.description.trim(),
      image: form.image.trim() || null,
      photos: form.photos_csv ? form.photos_csv.split(/[,\n]/).map(s => s.trim()).filter(Boolean) : [],
      status: 'draft',
    };
    let token = ''; try { token = localStorage.getItem('tlb_host_token') || localStorage.getItem('tlb_token') || ''; } catch {}
    fetch('/api/properties', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
      body: JSON.stringify(payload),
    })
      .then(r => r.json().then(j => ({ ok: r.ok, j })))
      .then(({ ok, j }) => {
        if (!ok) throw new Error(j.error || 'Failed to create villa');
        showToast(`"${form.name}" created as a draft. Add photos to publish.`);
        onClose();
        setStep(0); setForm({ name: '', location: 'Seminyak', type: 'villa', bedrooms: 3, bathrooms: 3, guests: 6, nightly_rate: 5000000, currency: 'IDR', min_nights: 2, description: '', image: '', photos_csv: '' });
        window.dispatchEvent(new CustomEvent('tlb-villa-created'));
      })
      .catch(e => { setErr(e.message); showToast('Could not create villa: ' + e.message, 'danger'); })
      .finally(() => setBusy(false));
  }

  return (
    <Modal open={open} onClose={onClose} title="Add a villa" subtitle="Three steps · saved as draft you can publish anytime" width={680}>
      <StepDots current={step} total={3} />

      {step === 0 && (
        <div>
          <Field label="Villa name" required>
            <Input value={form.name} onChange={set('name')} placeholder="e.g. Villa Akar Seminyak" autoFocus />
          </Field>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <Field label="Location" required>
              <Select value={form.location} onChange={set('location')}>
                {['Seminyak','Canggu','Ubud','Uluwatu','Jimbaran','Nusa Dua','Sanur','Lovina','Tabanan','Sidemen','Amed','Pererenan','Pecatu','Lombok','Nusa Penida'].map(l => <option key={l} value={l}>{l}</option>)}
              </Select>
            </Field>
            <Field label="Property type">
              <Select value={form.type} onChange={set('type')}>
                <option value="villa">Villa</option>
                <option value="estate">Estate</option>
                <option value="apartment">Apartment</option>
                <option value="resort">Boutique resort</option>
              </Select>
            </Field>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
            <Field label="Bedrooms" required><Input type="number" min={1} max={20} value={form.bedrooms} onChange={setNum('bedrooms')} /></Field>
            <Field label="Bathrooms" required><Input type="number" min={1} max={20} value={form.bathrooms} onChange={setNum('bathrooms')} /></Field>
            <Field label="Sleeps" required><Input type="number" min={1} max={40} value={form.guests} onChange={setNum('guests')} /></Field>
          </div>
        </div>
      )}

      {step === 1 && (
        <div>
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 14 }}>
            <Field label="Nightly rate" required hint="Base rate. Add seasonal lifts in Pricing later.">
              <Input type="number" min={100000} step={100000} value={form.nightly_rate} onChange={setNum('nightly_rate')} />
            </Field>
            <Field label="Currency"><Select value={form.currency} onChange={set('currency')}><option>IDR</option><option>USD</option></Select></Field>
          </div>
          <Field label="Minimum stay (nights)" hint="2 is typical for Bali; 5 for peak season; 7 for Christmas/NYE.">
            <Input type="number" min={1} max={30} value={form.min_nights} onChange={setNum('min_nights')} />
          </Field>
          <Panel pad={16} style={{ background: CC.offWhite, borderColor: 'transparent' }}>
            <Sans size={11} color={CC.mid} style={{ lineHeight: 1.6 }}>
              <strong style={{ color: CC.ink }}>Pricing tip:</strong> villas at this rate that hit 60%+ occupancy typically yield <span style={{ fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 14 }}>{fmtIDR(form.nightly_rate * 18)}</span>/month gross.
            </Sans>
          </Panel>
        </div>
      )}

      {step === 2 && (
        <div>
          <Field label="Story / description" required hint="Editorial tone — what kind of guest is this villa for? What makes a morning here special?">
            <Textarea value={form.description} onChange={set('description')} placeholder="Three-bedroom escape on a quiet rice-paddy lane in Pererenan. Twenty-metre infinity pool, full chef's kitchen, sunrise yoga deck. Forty minutes from the airport, ten from the beach club crawl." />
            <Sans size={11} color={CC.mid} style={{ marginTop: 6 }}>{form.description.length} characters · 50 minimum</Sans>
          </Field>
          <Field label="Hero photo URL" hint="Used on search results + share previews. Must be a public URL (https://…).">
            <Input value={form.image} onChange={set('image')} placeholder="https://images.unsplash.com/…" />
          </Field>
          <Field label="Gallery photo URLs" hint="One per line, or comma-separated. We'll wire to drag-and-drop upload in v82.">
            <Textarea value={form.photos_csv} onChange={set('photos_csv')} placeholder="https://…/villa-pool.jpg&#10;https://…/villa-bedroom.jpg" />
          </Field>
        </div>
      )}

      {err && (
        <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c', marginTop: 16 }}>
          <Sans size={12} color="#9a1c1c">{err}</Sans>
        </Panel>
      )}

      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 24, gap: 12 }}>
        {step > 0 ? <SecondaryBtn onClick={() => setStep(s => s - 1)}>← Back</SecondaryBtn> : <span />}
        {step < 2
          ? <PrimaryBtn onClick={next}>Continue →</PrimaryBtn>
          : <PrimaryBtn onClick={submit} disabled={busy}>{busy ? 'Creating…' : 'Create draft villa'}</PrimaryBtn>}
      </div>
    </Modal>
  );
}

// ════════════════════════════════════════════════════════════════════════
// ADMIN · CREATE HOST MODAL (single step)
// ════════════════════════════════════════════════════════════════════════
// POSTs /api/admin-manage with action=create_host (same endpoint operational
// admin uses). Generates a temporary password the admin can copy and share.
function CreateHostModal({ open, onClose }) {
  const [busy, setBusy] = useState_M(false);
  const [err, setErr] = useState_M('');
  const [created, setCreated] = useState_M(null);
  const [form, setForm] = useState_M({ email: '', name: '', company: '', phone: '', plan: 'standard' });

  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });

  function submit() {
    if (!form.email.trim() || !form.name.trim()) { setErr('Email and name are required.'); return; }
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { setErr('Invalid email format.'); return; }
    setErr(''); setBusy(true);
    let token = ''; try { token = localStorage.getItem('tlb_admin_token') || localStorage.getItem('tlb_token') || ''; } catch {}
    fetch('/api/admin-manage', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
      body: JSON.stringify({ action: 'create_host', ...form }),
    })
      .then(r => r.json().then(j => ({ ok: r.ok, j })))
      .then(({ ok, j }) => {
        if (!ok) throw new Error(j.error || 'Failed to create host');
        setCreated({ email: form.email, name: form.name, password: j.host?.temp_password || j.password || j.host?.password });
        showToast(`Host "${form.name}" created.`);
        window.dispatchEvent(new CustomEvent('tlb-host-created'));
      })
      .catch(e => { setErr(e.message); showToast(e.message, 'danger'); })
      .finally(() => setBusy(false));
  }

  function copyPassword() {
    if (!created || !created.password) return;
    navigator.clipboard.writeText(created.password).then(() => showToast('Password copied to clipboard.'));
  }

  function reset() {
    setCreated(null); setErr(''); setForm({ email: '', name: '', company: '', phone: '', plan: 'standard' });
  }

  return (
    <Modal open={open} onClose={() => { onClose(); reset(); }} title={created ? 'Host created' : 'Add a host account'} subtitle={created ? 'Share the temp password securely — they can change it on first login' : 'They\'ll receive a welcome email with login instructions'}>
      {created ? (
        <div>
          <Panel pad={20} style={{ background: 'rgba(31,107,58,0.06)', borderColor: '#1f6b3a', marginBottom: 18 }}>
            <Sans size={11} weight={600} ls={0.14} upper color="#1f6b3a" style={{ marginBottom: 10 }}>Account ready</Sans>
            <div style={{ marginBottom: 12 }}>
              <Sans size={11} weight={600} ls={0.10} upper color={CC.mid}>Email</Sans>
              <Sans size={14} color={CC.ink} style={{ marginTop: 4, fontFamily: 'ui-monospace, monospace' }}>{created.email}</Sans>
            </div>
            {created.password && (
              <div>
                <Sans size={11} weight={600} ls={0.10} upper color={CC.mid}>Temporary password</Sans>
                <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginTop: 6 }}>
                  <code style={{ flex: 1, padding: '10px 14px', background: CC.white, border: `1px solid ${CC.border}`, borderRadius: 8, fontSize: 13, color: CC.ink }}>{created.password}</code>
                  <SecondaryBtn onClick={copyPassword}>Copy</SecondaryBtn>
                </div>
              </div>
            )}
          </Panel>
          <Sans size={12} color={CC.mid} style={{ lineHeight: 1.6, marginBottom: 18 }}>Share via WhatsApp or another secure channel — never email the password. The host can change it after first login.</Sans>
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
            <SecondaryBtn onClick={reset}>+ Add another</SecondaryBtn>
            <PrimaryBtn onClick={() => { onClose(); reset(); }}>Done</PrimaryBtn>
          </div>
        </div>
      ) : (
        <div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <Field label="Email" required><Input type="email" value={form.email} onChange={set('email')} placeholder="hello@villaname.com" autoFocus /></Field>
            <Field label="Display name" required><Input value={form.name} onChange={set('name')} placeholder="Putu Wijaya" /></Field>
            <Field label="Company"><Input value={form.company} onChange={set('company')} placeholder="Optional · channel partner name" /></Field>
            <Field label="WhatsApp"><Input value={form.phone} onChange={set('phone')} placeholder="+62 812 …" /></Field>
          </div>
          <Field label="Plan" hint="Standard for individual hosts; channel partner unlocks bulk import + iCal sync.">
            <Select value={form.plan} onChange={set('plan')}>
              <option value="standard">Standard host</option>
              <option value="channel_partner">Channel partner</option>
              <option value="enterprise">Enterprise (multi-property)</option>
            </Select>
          </Field>
          {err && <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c' }}><Sans size={12} color="#9a1c1c">{err}</Sans></Panel>}
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 18 }}>
            <SecondaryBtn onClick={onClose}>Cancel</SecondaryBtn>
            <PrimaryBtn onClick={submit} disabled={busy}>{busy ? 'Creating…' : 'Create host'}</PrimaryBtn>
          </div>
        </div>
      )}
    </Modal>
  );
}

// ════════════════════════════════════════════════════════════════════════
// HOST · EDIT SETTINGS MODAL
// ════════════════════════════════════════════════════════════════════════
// In-place edit of profile fields. Bank/tax fields gated to operational
// page for security.
function EditHostSettingsModal({ open, onClose, profile }) {
  const [busy, setBusy] = useState_M(false);
  const [err, setErr] = useState_M('');
  const [form, setForm] = useState_M({
    name: '', whatsapp: '', display_name: '', bio: '',
  });

  useEffect_M(() => {
    if (open && profile) {
      setForm({
        name: profile.name || profile.display_name || '',
        whatsapp: profile.whatsapp || profile.phone || '',
        display_name: profile.display_name || '',
        bio: profile.bio || '',
      });
    }
  }, [open, profile]);

  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });

  function submit() {
    setErr(''); setBusy(true);
    let token = ''; try { token = localStorage.getItem('tlb_host_token') || localStorage.getItem('tlb_token') || ''; } catch {}
    fetch('/api/host-profile', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
      body: JSON.stringify(form),
    })
      .then(r => r.json().then(j => ({ ok: r.ok, j })))
      .then(({ ok, j }) => {
        if (!ok) throw new Error(j.error || 'Failed to update profile');
        showToast('Profile updated.');
        onClose();
        window.dispatchEvent(new CustomEvent('tlb-profile-updated'));
      })
      .catch(e => { setErr(e.message); })
      .finally(() => setBusy(false));
  }

  return (
    <Modal open={open} onClose={onClose} title="Edit profile" subtitle="Bank + tax details remain in classic for security">
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        <Field label="Full name" required><Input value={form.name} onChange={set('name')} autoFocus /></Field>
        <Field label="Display name" hint="How guests see you. Defaults to first name."><Input value={form.display_name} onChange={set('display_name')} /></Field>
      </div>
      <Field label="WhatsApp"><Input value={form.whatsapp} onChange={set('whatsapp')} placeholder="+62 812 …" /></Field>
      <Field label="Bio" hint="Two-line story shown on your villa pages — what guests will love about hosting with you."><Textarea value={form.bio} onChange={set('bio')} placeholder="Three-generation Balinese family welcoming guests since 2018…" /></Field>
      {err && <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c' }}><Sans size={12} color="#9a1c1c">{err}</Sans></Panel>}
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 8 }}>
        <SecondaryBtn onClick={onClose}>Cancel</SecondaryBtn>
        <PrimaryBtn onClick={submit} disabled={busy}>{busy ? 'Saving…' : 'Save changes'}</PrimaryBtn>
      </div>
    </Modal>
  );
}

// ════════════════════════════════════════════════════════════════════════
// IMPORT FROM OTA MODAL (v83 — universal, all 6 channels)
// ════════════════════════════════════════════════════════════════════════
// Replaces the TTD-only modal. Now supports import from EVERY OTA
// channel partner via 4 input methods:
//
//   1. BULK URL paste — works for Airbnb, Booking.com, Elite Havens,
//      Nakula, The Luxe Nomad, and any direct villa page URL. Server
//      auto-detects the source from URL pattern, fetches via headless
//      scraper, parses the canonical schema (price, photos, amenities).
//   2. CSV UPLOAD — bulk import from spreadsheet. Compatible with the
//      template at /templates/villa-import-template.csv. Server validates
//      each row and returns per-row success/error.
//   3. PASTE HTML — for sites behind WAF (TTD, sometimes Booking.com
//      anti-bot). Right-click → View Source → paste full HTML. Server
//      parses Next.js RSC / OpenGraph / JSON-LD as available.
//   4. BOOKMARKLET — drag JS bookmark to bar, click on any source page
//      to import in your browser session (bypasses any server WAF).
//      Includes "Sweep All" for listing pages.
//
// Same /api/import-listing endpoint underneath — the operational
// classic dashboard already uses it. Zero schema divergence.
function ImportFromOTAModal({ open, onClose }) {
  const [tab, setTab]         = useState_M('bulk');           // bulk | csv | paste | bookmarklet
  const [bulkUrls, setUrls]   = useState_M('');
  const [pasteSource, setPS]  = useState_M('tropicaldoor');   // override for paste-HTML mode
  const [pasteHtml, setHtml]  = useState_M('');
  const [csvFile, setCsv]     = useState_M(null);
  const [busy, setBusy]       = useState_M(false);
  const [results, setResults] = useState_M([]);
  const [progress, setProg]   = useState_M({ done: 0, total: 0 });
  const [err, setErr]         = useState_M('');

  // ── OTA registry — drives source-picker + URL pattern detection ──
  const OTAS = [
    { key: 'tropicaldoor', label: 'The Tropical Door',  pattern: /thetropicaldoor\.com/i,  badge: 'Channel partner', fetchable: false, color: '#B8923A' },
    { key: 'luxenomad',    label: 'The Luxe Nomad',     pattern: /theluxenomad\.com/i,     badge: 'Channel partner', fetchable: true,  color: '#7C5BB8' },
    { key: 'elitehavens',  label: 'Elite Havens',       pattern: /elitehavens\.com/i,      badge: 'Channel partner', fetchable: true,  color: '#3A7C9A' },
    { key: 'nakula',       label: 'Nakula',             pattern: /nakula\.com/i,           badge: 'Channel partner', fetchable: true,  color: '#7C9A3A' },
    { key: 'tls',          label: 'The Luxury Signature', pattern: /theluxurysignature\.com/i, badge: 'Channel partner', fetchable: true, color: '#C9A227' },
    { key: 'airbnb',       label: 'Airbnb',             pattern: /airbnb\.com/i,           badge: 'Public OTA',      fetchable: true,  color: '#FF5A5F' },
    { key: 'booking',      label: 'Booking.com',        pattern: /booking\.com/i,          badge: 'Public OTA',      fetchable: false, color: '#003580' },
  ];

  // Helper — get auth token
  function authToken() {
    try { return localStorage.getItem('tlb_host_token') || localStorage.getItem('tlb_token') || ''; }
    catch { return ''; }
  }

  // Bookmarklet builders — generic so the URL pattern fits ANY OTA
  // (the server auto-detects source from the page's location.href).
  const apiOrigin = typeof window !== 'undefined' ? window.location.origin : '';
  // Round-15.8 — bookmarklets now do the FULL import flow: parse → save.
  // Previously they only POSTed to /api/import-listing (which parses
  // but doesn't persist) and showed a misleading "Imported" toast even
  // though nothing was saved to the database. Now they parse first,
  // then POST the result to /api/properties to actually create the row.
  const singleBookmarklet = "javascript:(async()=>{try{" +
    "const t=localStorage.getItem('tlb_host_token')||localStorage.getItem('tlb_token')||'';" +
    "if(!t){alert('Please sign in to TLB host dashboard first');return}" +
    "const h=document.documentElement.outerHTML;" +
    // Step 1 — parse via /api/import-listing
    "const pr=await fetch('" + apiOrigin + "/api/import-listing',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+t},body:JSON.stringify({url:location.href,html:h})});" +
    "const pj=await pr.json();" +
    "if(!pr.ok||!pj.success){throw new Error(pj.error||'Parse failed')}" +
    // Step 2 — POST parsed data to /api/properties (this actually saves)
    "const sr=await fetch('" + apiOrigin + "/api/properties',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+t},body:JSON.stringify(pj)});" +
    "const sj=await sr.json();" +
    "const m=document.createElement('div');" +
    "m.style.cssText='position:fixed;top:20px;right:20px;background:'+(sr.ok?'#1f6b3a':'#9a1c1c')+';color:#fff;padding:14px 18px;border-radius:100px;z-index:9999999;font:13px system-ui;max-width:340px';" +
    "m.textContent=sr.ok?('Saved · '+(pj.name||'Villa')+(pj.nightlyRate?' · Rp '+Number(pj.nightlyRate).toLocaleString('id-ID'):'')):('Save failed: '+(sj.error||sr.status));" +
    "document.body.appendChild(m);setTimeout(()=>m.remove(),6000);" +
    "}catch(e){alert('TLB import error: '+e.message)}})();";

  const sweepBookmarklet = "javascript:(async()=>{try{" +
    "const t=localStorage.getItem('tlb_host_token')||localStorage.getItem('tlb_token')||'';" +
    "if(!t){alert('Please sign in to TLB host dashboard first');return}" +
    "const sels=['a[href*=\"/villas/\"]','a[href*=\"/villa/\"]','a[href*=\"/properties/\"]','a[href*=\"/rooms/\"]','a[href*=\"/hotel/\"]'];" +
    "const links=[...new Set(sels.flatMap(s=>[...document.querySelectorAll(s)]).map(a=>new URL(a.href,location.href).href).filter(u=>!u.includes('#')))];" +
    "if(!confirm('Sweep '+links.length+' detected villa pages to your TLB account?'))return;" +
    "const ui=document.createElement('div');ui.style.cssText='position:fixed;bottom:20px;right:20px;background:#141414;color:#fff;padding:16px 20px;border-radius:14px;z-index:9999999;font:12px system-ui;min-width:280px';" +
    "document.body.appendChild(ui);" +
    "let ok=0,fail=0;" +
    "for(let i=0;i<links.length;i++){" +
    "ui.textContent='Sweeping '+(i+1)+' / '+links.length+' · '+ok+' ok · '+fail+' fail';" +
    "try{" +
    "const p=await fetch(links[i]).then(r=>r.text());" +
    "const pr=await fetch('" + apiOrigin + "/api/import-listing',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+t},body:JSON.stringify({url:links[i],html:p})});" +
    "const pj=await pr.json();" +
    "if(!pr.ok||!pj.success){fail++;continue}" +
    "const sr=await fetch('" + apiOrigin + "/api/properties',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+t},body:JSON.stringify(pj)});" +
    "sr.ok?ok++:fail++;" +
    "}catch{fail++}" +
    "}" +
    "ui.textContent='Done · '+ok+' saved · '+fail+' failed';" +
    "setTimeout(()=>ui.remove(),10000);" +
    "}catch(e){alert('Sweep error: '+e.message)}})();";

  // Detect OTA from URL (used to label the result row)
  function detectSource(url) {
    for (const o of OTAS) if (o.pattern.test(url)) return o.key;
    return 'unknown';
  }

  // ── Action 1 — Bulk URL import ────────────────────────────────────
  function startBulkUrlImport() {
    const urls = bulkUrls.split(/[\n\r]+/).map(l => l.trim()).filter(l => l.length > 10 && /^https?:\/\//i.test(l));
    if (urls.length === 0) { setErr('No valid URLs. Each line must start with http:// or https://'); return; }
    setErr(''); setBusy(true); setResults([]); setProg({ done: 0, total: urls.length });
    const token = authToken();
    const out = [];

    // Sequential to avoid hammering source servers + so the progress
    // counter ticks visibly. Skip TTD/Booking that need bookmarklet.
    (async () => {
      for (let i = 0; i < urls.length; i++) {
        const url = urls[i];
        const src = detectSource(url);
        const ota = OTAS.find(o => o.key === src);
        if (ota && !ota.fetchable) {
          out.push({ ok: false, url, source: src, error: 'WAF-protected — use Bookmarklet tab for ' + ota.label });
        } else {
          try {
            // Round-15.8 — 2-step: parse via /api/import-listing GET (server fetches),
            // then POST result to /api/properties to actually persist. Previously
            // bulk URL just parsed and returned, never saving — so users thought
            // their imports succeeded but nothing landed in the DB.
            // Step 1: parse (server-fetch via GET param)
            const pr = await fetch('/api/import-listing?url=' + encodeURIComponent(url), {
              headers: token ? { 'Authorization': 'Bearer ' + token } : {},
            });
            const pj = await pr.json();
            if (!pr.ok || !pj.success) {
              out.push({ ok: false, url, source: src, error: pj.error || pj.message || 'Parse failed' });
            } else {
              // Step 2: persist
              const sr = await fetch('/api/properties', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
                body: JSON.stringify(pj),
              });
              const sj = await sr.json();
              out.push({
                ok: sr.ok,
                url,
                source: src,
                name: pj.name,
                price_idr: pj.nightlyRate || pj.nightly_rate,
                error: sr.ok ? null : (sj.error || 'Save failed'),
              });
            }
          } catch (e) {
            out.push({ ok: false, url, source: src, error: e.message });
          }
        }
        setProg({ done: i + 1, total: urls.length });
        setResults([...out]);
      }
      setBusy(false);
      window.dispatchEvent(new CustomEvent('tlb-villa-created'));
    })();
  }

  // ── Action 2 — CSV upload ─────────────────────────────────────────
  function startCsvImport() {
    if (!csvFile) { setErr('Pick a CSV file first.'); return; }
    setErr(''); setBusy(true); setResults([]);
    const token = authToken();
    const reader = new FileReader();
    reader.onload = async (e) => {
      try {
        const r = await fetch('/api/import-csv', {
          method: 'POST',
          headers: { 'Content-Type': 'text/csv', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
          body: e.target.result,
        });
        const j = await r.json();
        if (!r.ok) { setErr(j.error || 'CSV import failed'); }
        else {
          setResults((j.rows || j.results || []).map(row => ({ ok: !row.error, name: row.name, error: row.error })));
          window.dispatchEvent(new CustomEvent('tlb-villa-created'));
        }
      } catch (err) { setErr(err.message); }
      finally { setBusy(false); }
    };
    reader.readAsText(csvFile);
  }

  // ── Action 3 — Paste HTML import (multi-villa via === separator) ──
  function startPasteImport() {
    if (!pasteHtml.trim() || pasteHtml.length < 200) { setErr('Paste the rendered HTML (right-click → View page source → copy all).'); return; }
    setErr(''); setBusy(true); setResults([]);
    const token = authToken();
    const blocks = pasteHtml.split(/\n[=]{3,}\n/g).filter(b => b.trim().length > 200);
    setProg({ done: 0, total: blocks.length });

    Promise.all(blocks.map((b, i) =>
      fetch('/api/import-listing', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
        body: JSON.stringify({ source: pasteSource, html: b }),
      })
        .then(r => r.json().then(j => ({ ok: r.ok, source: pasteSource, ...j })))
        .then(res => { setProg(p => ({ ...p, done: p.done + 1 })); return res; })
    )).then(arr => {
      setResults(arr);
      window.dispatchEvent(new CustomEvent('tlb-villa-created'));
    }).catch(e => setErr('Import failed: ' + e.message))
      .finally(() => setBusy(false));
  }

  const tabs = [
    { v: 'bulk',         label: 'Bulk URL' },
    { v: 'csv',          label: 'CSV upload' },
    { v: 'paste',        label: 'Paste HTML' },
    { v: 'bookmarklet',  label: 'Bookmarklet' },
  ];

  return (
    <Modal open={open} onClose={onClose} title="Import from OTA channels" subtitle="Airbnb · Booking.com · Elite Havens · Nakula · The Luxe Nomad · The Tropical Door" width={820}>
      {/* OTA badges row — visual indicator of supported channels */}
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 18, paddingBottom: 14, borderBottom: `1px solid ${CC.border}` }}>
        {OTAS.map(o => (
          <span key={o.key} style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '5px 12px', borderRadius: 100, fontSize: 11, fontWeight: 500,
            background: o.color + '12', color: o.color, border: '1px solid ' + o.color + '33',
          }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: o.color }} />
            {o.label}
          </span>
        ))}
      </div>

      {/* Method tabs */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 22, flexWrap: 'wrap' }}>
        {tabs.map(o => (
          <button key={o.v} onClick={() => { setTab(o.v); setErr(''); setResults([]); }}
            style={{
              padding: '8px 18px', borderRadius: 100, fontSize: 12, fontWeight: 600,
              border: '1px solid ' + (tab === o.v ? CC.ink : CC.border),
              background: tab === o.v ? CC.ink : CC.white,
              color: tab === o.v ? CC.white : CC.deep,
              cursor: 'pointer', letterSpacing: '0.04em',
            }}>{o.label}</button>
        ))}
      </div>

      {/* ── Tab 1: Bulk URL ── */}
      {tab === 'bulk' && (
        <div>
          <Field label="Paste villa URLs (one per line)" hint="Auto-detects source from URL. Works for Airbnb, Elite Havens, Nakula, The Luxe Nomad. TTD + Booking.com need the Bookmarklet tab (WAF-protected).">
            <Textarea
              value={bulkUrls}
              onChange={e => setUrls(e.target.value)}
              placeholder="https://www.airbnb.com/rooms/28299515&#10;https://www.elitehavens.com/villa-levi/seminyak&#10;https://nakula.com/villas/villa-saba&#10;https://www.theluxenomad.com/villa/seminyak/villa-eden"
              style={{ minHeight: 160, fontFamily: 'ui-monospace, monospace', fontSize: 12 }}
            />
          </Field>
          {progress.total > 0 && busy && (
            <Panel pad={12} style={{ background: CC.offWhite, borderColor: 'transparent', marginBottom: 12 }}>
              <Sans size={12} color={CC.deep}>Importing {progress.done} / {progress.total}…</Sans>
              <div style={{ height: 4, background: CC.border, borderRadius: 2, marginTop: 8, overflow: 'hidden' }}>
                <div style={{ width: (progress.done / progress.total * 100) + '%', height: '100%', background: CC.ink, transition: 'width .25s' }} />
              </div>
            </Panel>
          )}
          {err && <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c', marginBottom: 12 }}><Sans size={12} color="#9a1c1c">{err}</Sans></Panel>}
          <ResultsList results={results} OTAS={OTAS} />
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <SecondaryBtn onClick={onClose}>Close</SecondaryBtn>
            <PrimaryBtn onClick={startBulkUrlImport} disabled={busy || !bulkUrls.trim()}>{busy ? 'Importing…' : 'Start bulk import'}</PrimaryBtn>
          </div>
        </div>
      )}

      {/* ── Tab 2: CSV Upload ── */}
      {tab === 'csv' && (
        <div>
          <Field label="Upload CSV file" hint="Headers: name, location, bedrooms, bathrooms, guests, nightly_rate, currency, description, image, photos (semicolon-separated URLs). Need a template? POST /api/import-csv with action=template to receive a sample CSV.">
            <input
              type="file"
              accept=".csv,.tsv,text/csv,text/tab-separated-values"
              onChange={e => setCsv(e.target.files[0] || null)}
              style={{ width: '100%', padding: 14, border: `2px dashed ${CC.border}`, borderRadius: 12, background: CC.offWhite, cursor: 'pointer', fontSize: 13, fontFamily: F_TLB.ui }}
            />
            {csvFile && (
              <Sans size={11} color={CC.mid} style={{ marginTop: 8 }}>
                Selected: <strong style={{ color: CC.ink }}>{csvFile.name}</strong> ({(csvFile.size / 1024).toFixed(1)} KB)
              </Sans>
            )}
          </Field>
          {err && <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c', marginBottom: 12 }}><Sans size={12} color="#9a1c1c">{err}</Sans></Panel>}
          <ResultsList results={results} OTAS={OTAS} />
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <SecondaryBtn onClick={onClose}>Close</SecondaryBtn>
            <PrimaryBtn onClick={startCsvImport} disabled={busy || !csvFile}>{busy ? 'Uploading…' : 'Import CSV'}</PrimaryBtn>
          </div>
        </div>
      )}

      {/* ── Tab 3: Paste HTML ── */}
      {tab === 'paste' && (
        <div>
          <Field label="Source channel" hint="Pick the OTA you copied the HTML from. Server uses this to pick the right parser (Next.js RSC for TTD, OG/JSON-LD for others).">
            <Select value={pasteSource} onChange={e => setPS(e.target.value)}>
              {OTAS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
            </Select>
          </Field>
          <Field label="Paste rendered HTML" hint="Right-click on the villa page → View page source → Select all → Copy → paste below. For multiple villas, separate each block with a line of '==='">
            <Textarea
              value={pasteHtml}
              onChange={e => setHtml(e.target.value)}
              placeholder="Paste raw HTML here…"
              style={{ minHeight: 200, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 11 }}
            />
          </Field>
          {progress.total > 1 && busy && (
            <Sans size={11} color={CC.mid} style={{ marginBottom: 12 }}>Parsing {progress.done} / {progress.total} blocks…</Sans>
          )}
          {err && <Panel pad={12} style={{ background: '#fbf3f3', borderColor: '#9a1c1c', marginBottom: 12 }}><Sans size={12} color="#9a1c1c">{err}</Sans></Panel>}
          <ResultsList results={results} OTAS={OTAS} />
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <SecondaryBtn onClick={onClose}>Close</SecondaryBtn>
            <PrimaryBtn onClick={startPasteImport} disabled={busy || !pasteHtml.trim()}>{busy ? 'Importing…' : 'Import to my properties'}</PrimaryBtn>
          </div>
        </div>
      )}

      {/* ── Tab 4: Bookmarklet ── */}
      {tab === 'bookmarklet' && (
        <div>
          <Sans size={13} color={CC.deep} style={{ lineHeight: 1.6, marginBottom: 18 }}>
            Drag either button below to your <strong>browser bookmark bar</strong>. Then on any villa page (TTD, Airbnb, Booking.com, Elite Havens, etc.) click the bookmark — it runs in your browser session (which already has cookies + WAF tokens) and POSTs the data to TLB.
          </Sans>
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 22 }}>
            <a href={singleBookmarklet}
              draggable={true}
              onClick={(e) => e.preventDefault()}
              style={{ padding: '12px 20px', background: '#B8923A', color: '#fff', borderRadius: 100, textDecoration: 'none', fontSize: 13, fontWeight: 600, cursor: 'grab', userSelect: 'none' }}
              title="Drag to your bookmark bar"
            >📎 Import This Villa</a>
            <a href={sweepBookmarklet}
              draggable={true}
              onClick={(e) => e.preventDefault()}
              style={{ padding: '12px 20px', background: '#141414', color: '#fff', borderRadius: 100, textDecoration: 'none', fontSize: 13, fontWeight: 600, cursor: 'grab', userSelect: 'none' }}
              title="Drag to your bookmark bar"
            >📎 Sweep All Villas on Page</a>
          </div>
          <Panel pad={16} style={{ background: CC.offWhite, borderColor: 'transparent' }}>
            <Sans size={11} weight={600} ls={0.10} upper color={CC.mid} style={{ marginBottom: 10 }}>How it works</Sans>
            <Sans size={12} color={CC.deep} style={{ lineHeight: 1.7 }}>
              <strong>Import This Villa</strong>: open one villa page (any source) → click the bookmark → toast confirms <em>"Imported · Villa Name — Rp X/night"</em>. Server auto-detects the OTA from the URL.
              <br /><br />
              <strong>Sweep All Villas</strong>: open a listing/index page (e.g. <code style={{ fontSize: 11 }}>thetropicaldoor.com/villas</code>, <code style={{ fontSize: 11 }}>elitehavens.com/seminyak</code>) → click the bookmark → confirms count → loops through every detected villa link with a live progress panel.
            </Sans>
          </Panel>
          <Panel pad={14} style={{ background: 'rgba(184,146,58,0.06)', borderColor: 'rgba(184,146,58,0.3)', marginTop: 14 }}>
            <Sans size={11} weight={600} ls={0.10} upper color="#8a6324" style={{ marginBottom: 6 }}>Best for</Sans>
            <Sans size={12} color={CC.deep} style={{ lineHeight: 1.6 }}>
              Sites with WAF protection: <strong>The Tropical Door</strong>, <strong>Booking.com</strong> (sometimes), heavy SPA flows where server-side fetch returns just a loading shell.
            </Sans>
          </Panel>
        </div>
      )}
    </Modal>
  );
}

// Reusable result list — shows per-row success/error with OTA badge
function ResultsList({ results, OTAS }) {
  if (!results || !results.length) return null;
  const okCount   = results.filter(r => r.ok).length;
  const failCount = results.length - okCount;
  return (
    <Panel pad={16} style={{ background: 'rgba(31,107,58,0.04)', borderColor: failCount ? '#9a1c1c33' : '#1f6b3a33', marginTop: 12 }}>
      <Sans size={11} weight={600} ls={0.14} upper color={failCount ? '#8a6324' : '#1f6b3a'} style={{ marginBottom: 10 }}>
        Import results · {okCount} ok{failCount > 0 ? ` · ${failCount} failed` : ''}
      </Sans>
      <div style={{ maxHeight: 240, overflowY: 'auto' }}>
        {results.map((r, i) => {
          const ota = OTAS.find(o => o.key === r.source);
          return (
            <div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '6px 0', borderTop: i ? `1px solid ${CC.border}` : 'none' }}>
              <span style={{ color: r.ok ? '#1f6b3a' : '#9a1c1c', fontWeight: 700, flexShrink: 0, fontSize: 13 }}>{r.ok ? '✓' : '✗'}</span>
              {ota && (
                <span style={{ display: 'inline-block', padding: '2px 8px', borderRadius: 100, fontSize: 9, fontWeight: 600, background: ota.color + '15', color: ota.color, flexShrink: 0, marginTop: 2 }}>{ota.label}</span>
              )}
              <Sans size={12} color={r.ok ? CC.deep : '#9a1c1c'} style={{ flex: 1, lineHeight: 1.5, wordBreak: 'break-word' }}>
                {r.ok
                  ? (r.name || 'Villa') + (r.price_idr ? ' — Rp ' + Number(r.price_idr).toLocaleString('id-ID') : '')
                  : (r.url ? r.url.slice(0, 60) + '… — ' : '') + (r.error || 'Failed')}
              </Sans>
            </div>
          );
        })}
      </div>
    </Panel>
  );
}

// Backward-compat alias — older code may still reference ImportTtdModal.
const ImportTtdModal = ImportFromOTAModal;

// ════════════════════════════════════════════════════════════════════════
// Public API exposed to native section components
// ════════════════════════════════════════════════════════════════════════
window.TLB_NATIVE_MUTATIONS = {
  AddPropertyWizard,
  CreateHostModal,
  EditHostSettingsModal,
  ImportFromOTAModal,
  // Backward-compat alias — sections that still reference ImportTtdModal
  // (pre-v83 native-sections.jsx) get the same universal modal.
  ImportTtdModal,
  showToast,
  Modal,
  Field,
  Input,
  Textarea,
  Select,
  PrimaryBtn,
  SecondaryBtn,
};
