// Places — a private, Supabase-backed shelf of hotels, restaurants, bars and
// notable spots. Same tile grid as Objects; auto-ingest pulls city/country, type,
// price and a few ratings (Michelin, ~Google, Mr & Mrs Smith, other accolades).
// Filter by type and by been / want-to-go; click a tile for ratings + notes.
//
// Data: `places` table (20260611100000_places.sql), owner-only. Add by pasting a
// link (auto-classified by ingest-object, kind:'place'), or email a link with
// `add_to_places` in the subject for hands-off upload.

const PLACE_TYPES = ['Hotel', 'Restaurant', 'Bar', 'Cafe', 'Place'];

// Find a representative photo for a place with no uploaded/scraped image, using
// the keyless Wikipedia API (CORS-enabled via origin=*). Cached per name+city.
const _wikiImgCache = {};
const fetchWikiImage = async (name, city) => {
  const key = (name || '') + '|' + (city || '');
  if (_wikiImgCache[key] !== undefined) return _wikiImgCache[key];
  const titles = [name, city ? `${name}, ${city}` : null, city ? `${name} (${city})` : null].filter(Boolean);
  for (const t of titles) {
    try {
      const u = `https://en.wikipedia.org/w/api.php?action=query&format=json&origin=*&prop=pageimages&piprop=thumbnail&pithumbsize=1200&redirects=1&titles=${encodeURIComponent(t)}`;
      const r = await fetch(u);
      if (!r.ok) continue;
      const j = await r.json();
      const pages = (j.query && j.query.pages) || {};
      for (const k in pages) {
        const s = pages[k].thumbnail && pages[k].thumbnail.source;
        if (s) { _wikiImgCache[key] = s; return s; }
      }
    } catch { /* try next title */ }
  }
  _wikiImgCache[key] = null;
  return null;
};

const PlaceImage = ({ p }) => {
  const base = [p.image_override, p.image_url].filter(Boolean);
  const [idx, setIdx] = React.useState(0);
  const [wiki, setWiki] = React.useState(null);

  // No uploaded/scraped image → try Wikipedia so seeds & known spots still show a photo.
  React.useEffect(() => {
    let cancelled = false;
    setWiki(null); setIdx(0);
    if (!base.length && p.name) fetchWikiImage(p.name, p.city).then((s) => { if (!cancelled && s) setWiki(s); });
    return () => { cancelled = true; };
  }, [p.id, p.image_override, p.image_url]);

  const sources = base.length ? base : (wiki ? [wiki] : []);
  const src = sources[idx];
  if (!src) {
    return (
      <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${tokens.bgAlt}, ${tokens.paper})`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <span style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, textTransform: 'uppercase' }}>{p.place_type || 'Place'}</span>
      </div>
    );
  }
  return <img src={src} alt={p.name} loading="lazy" onError={() => setIdx((i) => i + 1)} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />;
};

// Compact rating chips used on tiles and in the detail panel.
const michelinLabel = (m) => !m ? null : /key/i.test(m) ? `🔑 ${m}` : /bib/i.test(m) ? `Bib Gourmand` : /star/i.test(m) ? `★ ${m}` : m;

// ── Map view (Leaflet via CDN — no API key; OpenStreetMap tiles) ─────────────
const PLACE_COLORS = { Hotel: '#1A4FB5', Restaurant: '#C9622E', Bar: '#7A2D8E', Cafe: '#1F6F5C', Place: '#9A938A' };

const ensureLeaflet = (ok, fail) => {
  if (window.L && window.L.map) return ok();
  if (!document.getElementById('leaflet-css')) {
    const css = document.createElement('link');
    css.id = 'leaflet-css'; css.rel = 'stylesheet';
    css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
    document.head.appendChild(css);
  }
  let s = document.getElementById('leaflet-js');
  if (!s) {
    s = document.createElement('script');
    s.id = 'leaflet-js';
    s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
    s.onload = () => ok();
    s.onerror = () => fail && fail();
    document.body.appendChild(s);
  } else if (window.L && window.L.map) ok();
  else { s.addEventListener('load', ok, { once: true }); s.addEventListener('error', () => fail && fail(), { once: true }); }
};

const PlacesMap = ({ places, onSelect }) => {
  const ref = React.useRef(null);
  const mapRef = React.useRef(null);
  const layerRef = React.useRef(null);
  const [failed, setFailed] = React.useState(false);
  const pinned = places.filter((p) => p.lat != null && p.lng != null);

  React.useEffect(() => {
    let cancelled = false;
    ensureLeaflet(() => {
      if (cancelled || !ref.current || !window.L) return;
      if (!mapRef.current) {
        mapRef.current = window.L.map(ref.current, { scrollWheelZoom: false, worldCopyJump: true });
        window.L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
          attribution: '© OpenStreetMap © CARTO', maxZoom: 18,
        }).addTo(mapRef.current);
        layerRef.current = window.L.layerGroup().addTo(mapRef.current);
        // The container may size after mount; recompute once Leaflet is in.
        setTimeout(() => { if (mapRef.current) mapRef.current.invalidateSize(); }, 60);
      }
      const layer = layerRef.current;
      layer.clearLayers();
      pinned.forEach((p) => {
        const color = PLACE_COLORS[p.place_type] || PLACE_COLORS.Place;
        const m = window.L.circleMarker([p.lat, p.lng], {
          radius: 8, color, weight: 2.5,
          // visited = filled pin; want-to-go = hollow ring
          fillColor: color, fillOpacity: p.visited === 'been' ? 0.85 : 0.1,
        }).addTo(layer);
        m.bindPopup(`<div style="font-family:Inter,sans-serif;font-size:13px;font-weight:600">${p.name}</div>` +
          `<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#7A736A">${[p.place_type, p.city, p.country].filter(Boolean).join(' · ')} · ${p.visited === 'been' ? '✓ BEEN' : '◷ WANT'}</div>`);
        m.on('click', () => onSelect && onSelect(p));
      });
      if (pinned.length) {
        mapRef.current.fitBounds(pinned.map((p) => [p.lat, p.lng]), { padding: [40, 40], maxZoom: 11 });
      } else {
        mapRef.current.setView([30, 0], 2);
      }
    }, () => { if (!cancelled) setFailed(true); });
    return () => { cancelled = true; };
  }, [JSON.stringify(pinned.map((p) => [p.id, p.lat, p.lng, p.visited]))]);

  // Tear the map down only on unmount (keeping it across filter changes).
  React.useEffect(() => () => { if (mapRef.current) { mapRef.current.remove(); mapRef.current = null; } }, []);

  if (failed) return <div style={{ padding: '24px', fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>Couldn't load the map library.</div>;
  return (
    <div>
      <div ref={ref} style={{ width: '100%', height: '480px', background: tokens.bgAlt, border: `1px solid ${tokens.inkLine}` }} />
      <div style={{ display: 'flex', gap: '18px', flexWrap: 'wrap', marginTop: '10px', fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.08em', color: tokens.inkMute }}>
        {Object.entries(PLACE_COLORS).map(([k, c]) => (
          <span key={k}><span style={{ display: 'inline-block', width: '9px', height: '9px', borderRadius: '50%', background: c, marginRight: '5px', verticalAlign: '-1px' }} />{k.toUpperCase()}</span>
        ))}
        <span>● FILLED = BEEN · ○ HOLLOW = WANT TO GO</span>
        {places.length > pinned.length && <span>· {places.length - pinned.length} WITHOUT COORDINATES NOT SHOWN</span>}
      </div>
    </div>
  );
};

const P_FIELD = { width: '100%', padding: '10px 12px', border: `1px solid ${tokens.inkLine}`, background: tokens.paper, fontSize: '13px', fontFamily: fontDisplay, marginTop: '4px' };
const P_LABEL = { fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.12em', color: tokens.inkMute, textTransform: 'uppercase' };

const PlaceForm = ({ initial, onSaved, onCancel }) => {
  const blank = { name: '', place_type: 'Restaurant', cuisine: '', city: '', country: '', neighborhood: '', description: '', notes: '', source_url: '', booking_url: '', image_url: '', image_override: '', visited: 'want', price: '', michelin: '', google_rating: '', mrsmith: '', lat: '', lng: '', ratings: {}, tags: [], size: 'md', featured: false, status: 'published', sort_order: 0, attributes: {} };
  const [o, setO] = React.useState({ ...blank, ...(initial || {}) });
  const [busy, setBusy] = React.useState('');
  const [err, setErr] = React.useState('');
  const set = (k, v) => setO((prev) => ({ ...prev, [k]: v }));
  const client = window._supabaseClient;

  const autofill = async () => {
    if (!o.source_url) { setErr('Paste a link first.'); return; }
    setErr(''); setBusy('autofill');
    try {
      const { data, error } = await client.functions.invoke('ingest-object', { body: { url: o.source_url, kind: 'place', preview: true } });
      let payload = data;
      if (error) { try { payload = await error.context.json(); } catch {} }
      if (!payload || payload.error) throw new Error(payload?.error || error?.message || 'Auto-fill failed');
      const p = payload.object || {};
      setO((prev) => ({ ...prev, ...p, source_url: o.source_url, tags: p.tags || [], ratings: p.ratings || {}, attributes: p.attributes || {}, google_rating: p.google_rating ?? '', lat: p.lat ?? '', lng: p.lng ?? '' }));
    } catch (e) { setErr(e.message); } finally { setBusy(''); }
  };

  const upload = async (file) => {
    if (!file) return;
    setErr(''); setBusy('upload');
    try {
      const ext = (file.name.split('.').pop() || 'jpg').toLowerCase();
      const path = `${(o.name || 'place').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40)}-${Date.now()}.${ext}`;
      const { error } = await client.storage.from('place-images').upload(path, file, { upsert: true, contentType: file.type });
      if (error) throw error;
      const { data } = client.storage.from('place-images').getPublicUrl(path);
      set('image_override', data.publicUrl);
    } catch (e) { setErr(e.message); } finally { setBusy(''); }
  };

  const save = async () => {
    if (!o.name) { setErr('Name is required.'); return; }
    setErr(''); setBusy('save');
    const row = { ...o,
      tags: Array.isArray(o.tags) ? o.tags : String(o.tags).split(',').map((s) => s.trim()).filter(Boolean),
      google_rating: o.google_rating === '' || o.google_rating == null ? null : Number(o.google_rating),
      lat: o.lat === '' || o.lat == null ? null : Number(o.lat),
      lng: o.lng === '' || o.lng == null ? null : Number(o.lng),
      updated_at: new Date().toISOString() };
    delete row.id; delete row.created_at; delete row.user_id;
    try {
      let res;
      if (o.id) res = await client.from('places').update(row).eq('id', o.id);
      else res = await client.from('places').insert(row);
      if (res.error) throw res.error;
      onSaved();
    } catch (e) { setErr(e.message); } finally { setBusy(''); }
  };

  const tagsStr = Array.isArray(o.tags) ? o.tags.join(', ') : (o.tags || '');

  return (
    <div style={{ border: `1px solid ${tokens.ink}`, background: tokens.paper, padding: '24px', marginBottom: '24px', borderRadius: '4px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' }}>
        <div style={{ fontFamily: fontMono, fontSize: '12px', letterSpacing: '0.14em', textTransform: 'uppercase' }}>{o.id ? 'Edit place' : 'Add place'}</div>
        <button onClick={onCancel} style={{ ...P_LABEL, background: 'none', border: 'none', cursor: 'pointer' }}>CLOSE ✕</button>
      </div>

      <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end', marginBottom: '16px' }}>
        <div style={{ flexGrow: 1 }}>
          <span style={P_LABEL}>Source link (hotel/restaurant page, guide, or Instagram URL)</span>
          <input value={o.source_url || ''} onChange={(e) => set('source_url', e.target.value)} placeholder="https://…" style={P_FIELD} />
        </div>
        <button onClick={autofill} disabled={!!busy} style={{ padding: '10px 16px', background: tokens.ink, color: tokens.bg, border: 'none', cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.08em', whiteSpace: 'nowrap' }}>
          {busy === 'autofill' ? 'READING…' : '✦ AUTO-FILL'}
        </button>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 16px' }}>
        <label><span style={P_LABEL}>Name</span><input value={o.name} onChange={(e) => set('name', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Type</span>
          <select value={o.place_type} onChange={(e) => set('place_type', e.target.value)} style={P_FIELD}>
            {PLACE_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
          </select>
        </label>
        <label><span style={P_LABEL}>City</span><input value={o.city || ''} onChange={(e) => set('city', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Country</span><input value={o.country || ''} onChange={(e) => set('country', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Neighborhood</span><input value={o.neighborhood || ''} onChange={(e) => set('neighborhood', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Cuisine / style</span><input value={o.cuisine || ''} onChange={(e) => set('cuisine', e.target.value)} style={P_FIELD} /></label>
        <label style={{ gridColumn: 'span 2' }}><span style={P_LABEL}>Description</span><textarea value={o.description || ''} onChange={(e) => set('description', e.target.value)} rows={2} style={{ ...P_FIELD, resize: 'vertical' }} /></label>
        <label style={{ gridColumn: 'span 2' }}><span style={P_LABEL}>Notes (add over time)</span><textarea value={o.notes || ''} onChange={(e) => set('notes', e.target.value)} rows={2} style={{ ...P_FIELD, resize: 'vertical' }} placeholder="Tables to request, dishes, who recommended it…" /></label>
        <label><span style={P_LABEL}>Status</span>
          <select value={o.visited} onChange={(e) => set('visited', e.target.value)} style={P_FIELD}>
            <option value="want">Want to go</option><option value="been">Been</option>
          </select>
        </label>
        <label><span style={P_LABEL}>Price</span>
          <select value={o.price || ''} onChange={(e) => set('price', e.target.value)} style={P_FIELD}>
            <option value="">—</option><option>$</option><option>$$</option><option>$$$</option><option>$$$$</option>
          </select>
        </label>
        <label><span style={P_LABEL}>Michelin</span><input value={o.michelin || ''} onChange={(e) => set('michelin', e.target.value)} placeholder="One Star / Bib / One Key" style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Google rating</span><input value={o.google_rating ?? ''} onChange={(e) => set('google_rating', e.target.value)} placeholder="4.6" style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Mr &amp; Mrs Smith</span><input value={o.mrsmith || ''} onChange={(e) => set('mrsmith', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Booking / reservation link</span><input value={o.booking_url || ''} onChange={(e) => set('booking_url', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Latitude (for the map)</span><input value={o.lat ?? ''} onChange={(e) => set('lat', e.target.value)} placeholder="48.8740" style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Longitude</span><input value={o.lng ?? ''} onChange={(e) => set('lng', e.target.value)} placeholder="2.3001" style={P_FIELD} /></label>
        <label style={{ gridColumn: 'span 2' }}><span style={P_LABEL}>Tags (comma-separated)</span><input value={tagsStr} onChange={(e) => set('tags', e.target.value.split(',').map((s) => s.trim()).filter(Boolean))} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Stock image URL</span><input value={o.image_url || ''} onChange={(e) => set('image_url', e.target.value)} style={P_FIELD} /></label>
        <label><span style={P_LABEL}>Tile size</span>
          <select value={o.size} onChange={(e) => set('size', e.target.value)} style={P_FIELD}>
            <option value="sm">Small</option><option value="md">Medium</option><option value="lg">Large (hero)</option>
          </select>
        </label>
        <label style={{ gridColumn: 'span 2' }}>
          <span style={P_LABEL}>High-res override image {o.image_override ? '✓ set' : '(upload to override the stock image)'}</span>
          <input type="file" accept="image/*" onChange={(e) => upload(e.target.files[0])} style={{ ...P_FIELD, padding: '8px' }} />
        </label>
      </div>

      {err && <div style={{ marginTop: '12px', fontFamily: fontMono, fontSize: '11px', color: tokens.red }}>{err}</div>}

      <div style={{ display: 'flex', gap: '10px', marginTop: '18px', alignItems: 'center' }}>
        <button onClick={save} disabled={!!busy} style={{ padding: '11px 20px', background: tokens.ink, color: tokens.bg, border: 'none', cursor: 'pointer', fontSize: '13px', fontFamily: fontDisplay }}>
          {busy === 'save' ? 'Saving…' : (o.id ? 'Update place' : 'Add place')}
        </button>
        <label style={{ ...P_LABEL, display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
          <input type="checkbox" checked={!!o.featured} onChange={(e) => set('featured', e.target.checked)} /> FEATURED
        </label>
        {busy === 'upload' && <span style={{ ...P_LABEL }}>UPLOADING IMAGE…</span>}
      </div>
    </div>
  );
};

// Expanded detail shown below the grid when a tile is clicked.
const PlaceDetail = ({ p, onClose }) => {
  const isMobile = useIsMobile();
  const ratingChips = [];
  if (p.michelin) ratingChips.push(['Michelin', michelinLabel(p.michelin)]);
  if (p.google_rating) ratingChips.push(['Google', `${p.google_rating}★${p.google_reviews ? ` · ${p.google_reviews}` : ''}`]);
  if (p.mrsmith) ratingChips.push(['Mr & Mrs Smith', p.mrsmith]);
  Object.entries(p.ratings || {}).forEach(([k, v]) => ratingChips.push([k, String(v)]));
  const link = p.booking_url || p.source_url;
  return (
    <div style={{ border: `1px solid ${tokens.ink}`, background: tokens.paper, marginTop: '12px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1.1fr 1.6fr' }}>
      <div style={{ position: 'relative', minHeight: isMobile ? '200px' : '260px', background: tokens.bgAlt }}>
        <PlaceImage p={p} />
      </div>
      <div style={{ padding: '22px 24px' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '12px' }}>
          <div>
            <div style={{ fontSize: '26px', letterSpacing: '-0.03em', lineHeight: 1.1 }}>{p.name}</div>
            <div style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, letterSpacing: '0.06em', marginTop: '6px', textTransform: 'uppercase' }}>
              {[p.place_type, p.cuisine, [p.neighborhood, p.city, p.country].filter(Boolean).join(', ')].filter(Boolean).join(' · ')}
            </div>
          </div>
          <button onClick={onClose} style={{ ...P_LABEL, background: 'none', border: 'none', cursor: 'pointer' }}>CLOSE ✕</button>
        </div>

        <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '14px' }}>
          <span style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.08em', color: p.visited === 'been' ? tokens.green : tokens.ochre, border: `1px solid ${p.visited === 'been' ? tokens.green : tokens.ochre}`, borderRadius: '999px', padding: '4px 11px', textTransform: 'uppercase' }}>{p.visited === 'been' ? '✓ Been' : '◷ Want to go'}</span>
          {p.price && <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, border: `1px solid ${tokens.inkLine}`, borderRadius: '999px', padding: '4px 11px' }}>{p.price}</span>}
          {ratingChips.map(([k, v], i) => (
            <span key={i} style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ink, border: `1px solid ${tokens.inkLine}`, borderRadius: '999px', padding: '4px 11px' }}>
              <span style={{ color: tokens.inkMute }}>{k}:</span> {v}
            </span>
          ))}
        </div>

        {p.description && <p style={{ fontSize: '15px', lineHeight: 1.6, color: tokens.inkSoft, marginTop: '16px', marginBottom: 0 }}>{p.description}</p>}
        {p.notes && (
          <div style={{ marginTop: '14px', padding: '12px 14px', background: tokens.bgAlt, borderLeft: `2px solid ${tokens.ochre}` }}>
            <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '5px' }}>NOTES</div>
            <div style={{ fontSize: '14px', lineHeight: 1.55, color: tokens.ink, whiteSpace: 'pre-wrap' }}>{p.notes}</div>
          </div>
        )}
        {p.tags && p.tags.length > 0 && (
          <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '14px' }}>
            {p.tags.map((t, i) => <span key={i} style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', color: tokens.inkMute }}>#{t}</span>)}
          </div>
        )}
        {link && (
          <a href={link} target="_blank" rel="noopener noreferrer" style={{ display: 'inline-block', marginTop: '18px', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.08em', color: tokens.ink, borderBottom: `1px solid ${tokens.ink}`, paddingBottom: '3px' }}>
            {p.booking_url ? 'BOOK / RESERVE ↗' : 'VISIT SOURCE ↗'}
          </a>
        )}
      </div>
    </div>
  );
};

const Places = ({ isOwner }) => {
  const isMobile = useIsMobile();
  const [places, setPlaces] = React.useState(null);
  const [typeFilter, setTypeFilter] = React.useState('all');
  const [statusFilter, setStatusFilter] = React.useState('all'); // all | been | want
  const [editing, setEditing] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [loadErr, setLoadErr] = React.useState('');

  const load = React.useCallback(async () => {
    const client = window._supabaseClient;
    if (!client) { setPlaces([]); return; }
    const { data, error } = await client.from('places').select('*').order('featured', { ascending: false }).order('sort_order', { ascending: false }).order('created_at', { ascending: false });
    if (error) { setLoadErr(error.message); setPlaces([]); }
    else setPlaces(data || []);
  }, []);
  React.useEffect(() => { load(); }, [load]);

  const types = React.useMemo(() => {
    const set = new Set((places || []).map((p) => p.place_type).filter(Boolean));
    return ['all', ...Array.from(set)];
  }, [places]);

  const filtered = (places || []).filter((p) =>
    (typeFilter === 'all' || p.place_type === typeFilter) &&
    (statusFilter === 'all' || p.visited === statusFilter));

  const span = (s) => {
    if (isMobile) return (s === 'lg' || s === 'md') ? { gridColumn: 'span 2', aspectRatio: '2 / 1' } : { gridColumn: 'span 1', aspectRatio: '1' };
    if (s === 'lg') return { gridColumn: 'span 2', gridRow: 'span 2', aspectRatio: '1' };
    if (s === 'md') return { gridColumn: 'span 2', gridRow: 'span 1', aspectRatio: '2 / 1' };
    return { gridColumn: 'span 1', gridRow: 'span 1', aspectRatio: '1' };
  };

  const del = async (id) => {
    if (!roleCanDelete()) { window.alert(DEMO_BLOCK_MSG); return; }
    if (!window.confirm('Delete this place?')) return;
    await window._supabaseClient.from('places').delete().eq('id', id);
    if (selected && selected.id === id) setSelected(null);
    load();
  };

  const statusTab = (key, label) => (
    <button onClick={() => setStatusFilter(key)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: statusFilter === key ? tokens.ink : tokens.inkMute, borderBottom: statusFilter === key ? `1px solid ${tokens.ink}` : '1px solid transparent', padding: '6px 0', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.12em', textTransform: 'uppercase' }}>{label}</button>
  );

  return (
    <div>
      <section style={{ padding: '80px 56px 40px', borderBottom: `1px solid ${tokens.inkLine}` }}>
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.18em', color: tokens.inkMute, marginBottom: '24px' }}>08 · PLACES</div>
        <h1 style={{ fontSize: '88px', lineHeight: 0.92, letterSpacing: '-0.05em', fontWeight: 400, margin: 0, marginBottom: '32px' }}>Places.</h1>
        <p style={{ fontSize: '22px', lineHeight: 1.45, letterSpacing: '-0.015em', color: tokens.inkSoft, maxWidth: '780px' }}>
          A running map of hotels, restaurants, and bars — places I've been and places I want to go. Added by forwarding a link; ratings and city pulled in automatically, notes added over time.
        </p>
      </section>

      {/* Filter bar */}
      <section style={{ padding: '20px 56px', borderBottom: `1px solid ${tokens.inkLine}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '16px', flexWrap: 'wrap', position: 'sticky', top: isMobile ? 0 : 56, zIndex: 20, background: 'rgba(244,241,234,0.92)', backdropFilter: 'blur(10px)' }}>
        <div style={{ display: 'flex', gap: '18px', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.12em', flexWrap: 'wrap', alignItems: 'center' }}>
          {types.map((c) => (
            <button key={c} onClick={() => setTypeFilter(c)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: typeFilter === c ? tokens.ink : tokens.inkMute, borderBottom: typeFilter === c ? `1px solid ${tokens.ink}` : '1px solid transparent', padding: '6px 0', letterSpacing: '0.12em', textTransform: 'uppercase' }}>{c === 'all' ? 'All' : c}</button>
          ))}
          <span style={{ color: tokens.inkLine }}>|</span>
          {statusTab('all', 'Any')}{statusTab('been', '✓ Been')}{statusTab('want', '◷ Want')}
        </div>
        {isOwner && (
          <button onClick={() => { setEditing(editing ? null : {}); setSelected(null); }} style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.1em', color: tokens.bg, background: tokens.ink, border: 'none', padding: '8px 14px', cursor: 'pointer' }}>
            {editing ? '✕ CLOSE' : '＋ ADD PLACE'}
          </button>
        )}
      </section>

      {isOwner && editing && (
        <section style={{ padding: isMobile ? '20px 18px 0' : '24px 56px 0' }}>
          <PlaceForm initial={editing.id ? editing : {}} onSaved={() => { setEditing(null); load(); }} onCancel={() => setEditing(null)} />
        </section>
      )}

      {/* Detail panel for the selected place */}
      {selected && (
        <section style={{ padding: isMobile ? '12px 18px 0' : '16px 56px 0' }}>
          <PlaceDetail p={selected} onClose={() => setSelected(null)} />
        </section>
      )}

      {/* Grid */}
      <section style={{ padding: isMobile ? '12px' : '24px' }}>
        {places === null ? (
          <div style={{ padding: '80px', textAlign: 'center', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute }}>LOADING…</div>
        ) : filtered.length === 0 ? (
          <div style={{ padding: '80px', textAlign: 'center', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.12em', color: tokens.inkMute }}>
            {loadErr ? `COULDN'T LOAD — ${loadErr.toUpperCase()}` : 'NO PLACES YET'}
          </div>
        ) : (
          <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(4, minmax(0, 1fr))', gridAutoRows: isMobile ? 'minmax(150px, auto)' : 'minmax(220px, auto)', gap: '4px' }}>
            {filtered.map((p) => {
              const mich = michelinLabel(p.michelin);
              return (
                <div key={p.id} onClick={() => { setSelected(p); window.scrollTo({ top: 0, behavior: 'smooth' }); }} className="object-tile" style={{ ...span(p.size), position: 'relative', background: tokens.paper, overflow: 'hidden', cursor: 'pointer' }}>
                  <PlaceImage p={p} />

                  <div style={{ position: 'absolute', top: '14px', left: '14px', display: 'flex', gap: '6px', flexWrap: 'wrap', maxWidth: '75%' }}>
                    <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.16em', padding: '5px 8px', background: 'rgba(251,250,246,0.92)', color: tokens.ink, fontWeight: 500, textTransform: 'uppercase' }}>{p.place_type}</span>
                    <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.1em', padding: '5px 8px', background: p.visited === 'been' ? 'rgba(58,110,77,0.92)' : 'rgba(201,162,74,0.92)', color: '#fff', fontWeight: 500, textTransform: 'uppercase' }}>{p.visited === 'been' ? 'BEEN' : 'WANT'}</span>
                  </div>

                  {isOwner && (
                    <div style={{ position: 'absolute', top: '12px', right: '12px', display: 'flex', gap: '6px', zIndex: 3 }}>
                      <button onClick={(e) => { e.stopPropagation(); setEditing(p); setSelected(null); window.scrollTo({ top: 0, behavior: 'smooth' }); }} style={{ width: '26px', height: '26px', borderRadius: '50%', border: 'none', background: 'rgba(251,250,246,0.95)', cursor: 'pointer', fontSize: '11px' }}>✎</button>
                      {roleCanDelete() && <button onClick={(e) => { e.stopPropagation(); del(p.id); }} style={{ width: '26px', height: '26px', borderRadius: '50%', border: 'none', background: 'rgba(160,74,62,0.95)', color: '#fff', cursor: 'pointer', fontSize: '11px' }}>✕</button>}
                    </div>
                  )}

                  <div className="object-caption" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '18px', background: 'linear-gradient(to top, rgba(14,14,12,0.92) 0%, rgba(14,14,12,0.6) 55%, transparent 100%)', color: tokens.bg }}>
                    {[p.city, p.country].filter(Boolean).length > 0 && (
                      <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.16em', color: 'rgba(244,241,234,0.6)', marginBottom: '6px', textTransform: 'uppercase' }}>
                        {[p.city, p.country].filter(Boolean).join(' · ')}
                      </div>
                    )}
                    <div style={{ fontSize: p.size === 'lg' ? '20px' : '15px', letterSpacing: '-0.02em', lineHeight: 1.2, marginBottom: '6px' }}>{p.name}</div>
                    <div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap', fontFamily: fontMono, fontSize: '10px', color: 'rgba(244,241,234,0.7)' }}>
                      {mich && <span>{mich}</span>}
                      {p.google_rating && <span>G {p.google_rating}★</span>}
                      {p.price && <span>{p.price}</span>}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </section>

      {/* Map — pins follow the active filters; click a pin to open the detail */}
      {(places || []).length > 0 && (
        <section style={{ padding: isMobile ? '8px 12px 24px' : '8px 24px 40px' }}>
          <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, margin: '16px 0 12px' }}>THE MAP</div>
          <PlacesMap places={filtered} onSelect={(p) => { setSelected(p); window.scrollTo({ top: 0, behavior: 'smooth' }); }} />
        </section>
      )}

      <section style={{ padding: '60px 56px', borderTop: `1px solid ${tokens.inkLine}`, background: tokens.bgAlt }}>
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, marginBottom: '16px' }}>ON THIS MAP</div>
        <p style={{ fontSize: '15px', lineHeight: 1.6, color: tokens.inkSoft, letterSpacing: '-0.005em', maxWidth: '560px', margin: 0 }}>
          A living list — forward a link (or email one with “add_to_places” in the subject) and it's filed with city, type, and ratings. Filter by type, or by where I've been vs where I want to go.
        </p>
      </section>
    </div>
  );
};

window.Places = Places;
