// Gotanda Guide — Google Places integration (browser-side, Maps JS API)
// Loads Maps JS + Places library on demand, runs Text Search around Gotanda,
// hydrates each result with Place Details + photo URLs, and converts the
// results into our internal Spot shape.

const { useState: usePlaceState, useEffect: usePlaceEffect, useRef: usePlaceRef } = React;

const GOTANDA_LATLNG = { lat: 35.6262, lng: 139.7232 };
const GOTANDA_AREA_NAME = '五反田';
const GOOGLE_KEY_LS = 'gg_google_key';

// Map our internal categories → Google Places type / keyword combos
const CAT_QUERY = {
  izakaya:  { keyword: '居酒屋', type: 'restaurant' },
  ramen:    { keyword: 'ラーメン', type: 'restaurant' },
  sushi:    { keyword: '寿司', type: 'restaurant' },
  cafe:     { keyword: 'カフェ', type: 'cafe' },
  bar:      { keyword: 'バー', type: 'bar' },
  hotel:    { keyword: 'ホテル', type: 'lodging' },
  shop:     { keyword: 'ショップ', type: 'store' },
  sight:    { keyword: '観光', type: 'tourist_attraction' },
  work:     { keyword: 'コワーキング', type: 'establishment' },
};

// Google's textSearch is hard-capped at ~20 results per query and pagination
// (`nextPage`) is no longer functional for keys created after March 2025.
// To approximate "give me 60 spots" we fan out across several related
// keywords per category and dedupe by place_id.
const CAT_KEYWORDS = {
  izakaya: ['居酒屋', '焼き鳥', '炭火焼', '酒場', '小料理', 'ダイニングバー', '焼肉', 'ホルモン', 'もつ焼き'],
  ramen:   ['ラーメン', 'らーめん', 'つけ麺', '中華そば', '中華蕎麦', '担々麺', '油そば', '家系'],
  sushi:   ['寿司', '鮨', 'すし', '回転寿司', '海鮮', '魚料理'],
  cafe:    ['カフェ', '喫茶店', 'コーヒー', 'ベーカリー', 'スイーツ', 'カフェ&バー'],
  bar:     ['バー', 'ワインバー', 'ウイスキー', 'カクテル', 'クラフトビール', '立ち飲み', 'ダイニングバー'],
  hotel:   ['ホテル', 'ビジネスホテル', '旅館', '宿泊'],
  shop:    ['雑貨', '書店', 'セレクトショップ', '土産', 'ショッピング'],
  sight:   ['観光', '神社', '寺', '公園', '美術館', 'ギャラリー', '名所'],
  work:    ['コワーキング', 'シェアオフィス', 'レンタルオフィス'],
};

// Loader: injects the Maps JS script with the Places library exactly once.
let _mapsLoading = null;
function loadGoogleMaps(apiKey) {
  if (window.google?.maps?.places) return Promise.resolve(window.google);
  if (_mapsLoading) return _mapsLoading;
  _mapsLoading = new Promise((resolve, reject) => {
    const cb = '__ggMapsCb' + Date.now();
    window[cb] = () => { resolve(window.google); delete window[cb]; };
    const s = document.createElement('script');
    s.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&libraries=places&callback=${cb}&language=ja&region=JP`;
    s.async = true;
    s.onerror = (e) => { reject(new Error('Failed to load Google Maps JS')); _mapsLoading = null; };
    document.head.appendChild(s);
  });
  return _mapsLoading;
}

// PlacesService requires an HTMLElement to attach attribution to.
let _placesService = null;
function getPlacesService(google) {
  if (_placesService) return _placesService;
  const div = document.createElement('div');
  div.id = '__gg_places_attribution';
  document.body.appendChild(div);
  _placesService = new google.maps.places.PlacesService(div);
  return _placesService;
}

// One text search, up to ~20 results. Pagination is intentionally removed
// because nextPage() returns INVALID_REQUEST on Google API keys provisioned
// after the legacy PlacesService deprecation; the multi-keyword fan-out in
// searchAndConvert is what scales us past 20.
function textSearch(google, request) {
  const svc = getPlacesService(google);
  const PS = google.maps.places.PlacesServiceStatus;
  return new Promise((resolve, reject) => {
    svc.textSearch(request, (results, status) => {
      if (status === PS.OK || status === PS.ZERO_RESULTS) {
        resolve(results || []);
      } else {
        reject(new Error('Places textSearch: ' + status));
      }
    });
  });
}

function placeDetails(google, placeId) {
  const svc = getPlacesService(google);
  return new Promise((resolve, reject) => {
    svc.getDetails({
      placeId,
      fields: [
        'place_id', 'name', 'formatted_address', 'geometry',
        'rating', 'user_ratings_total', 'price_level',
        'opening_hours', 'photos', 'reviews',
        'types', 'website', 'international_phone_number', 'url',
      ],
    }, (place, status) => {
      if (status === google.maps.places.PlacesServiceStatus.OK) resolve(place);
      else reject(new Error('Places getDetails: ' + status));
    });
  });
}

// Map a Place → our Spot shape
function placeToSpot(p, fallbackCat) {
  const photos = (p.photos || []).slice(0, 5).map((ph) =>
    typeof ph.getUrl === 'function' ? ph.getUrl({ maxWidth: 1200 }) : ''
  ).filter(Boolean);
  const types = p.types || [];
  const cat = fallbackCat
    || (types.includes('cafe') ? 'cafe'
        : types.includes('bar') ? 'bar'
        : types.includes('lodging') ? 'hotel'
        : types.includes('store') ? 'shop'
        : types.includes('tourist_attraction') ? 'sight'
        : types.includes('restaurant') ? 'izakaya'
        : 'sight');
  const loc = p.geometry?.location;
  const lat = typeof loc?.lat === 'function' ? loc.lat() : (loc?.lat ?? GOTANDA_LATLNG.lat);
  const lng = typeof loc?.lng === 'function' ? loc.lng() : (loc?.lng ?? GOTANDA_LATLNG.lng);
  // crude walk minutes from Gotanda station
  const dKm = haversine(GOTANDA_LATLNG.lat, GOTANDA_LATLNG.lng, lat, lng);
  const walk = Math.max(1, Math.round(dKm / 0.08));
  return {
    id: p.place_id,
    cat,
    name: { ja: p.name || '', en: p.name || '' },
    tag: { ja: types[0] || '', en: types[0] || '' },
    desc: {
      ja: p.formatted_address || '',
      en: p.formatted_address || '',
    },
    price: typeof p.price_level === 'number' && p.price_level > 0
      ? '¥'.repeat(p.price_level)
      : '',
    hours: {
      ja: p.opening_hours?.weekday_text?.[0] || '',
      en: p.opening_hours?.weekday_text?.[0] || '',
    },
    walk,
    rating: p.rating || 4.0,
    coords: [lat, lng],
    address: { ja: p.formatted_address || '', en: p.formatted_address || '' },
    image: photos[0] || '',
    photos,
    website: p.website || p.url || '',
    phone: p.international_phone_number || '',
    tags: types.slice(0, 4),
    img: 0,
    _googleAttrib: true,
    userRatingsTotal: p.user_ratings_total || 0,
    reviews: (p.reviews || []).slice(0, 5).map((r) => ({
      author: r.author_name || '',
      avatar: r.profile_photo_url || '',
      authorUrl: r.author_url || '',
      rating: r.rating || 0,
      text: r.text || '',
      relative: r.relative_time_description || '',
      time: r.time || 0,
      lang: r.language || '',
    })),
    reviewsFetchedAt: Date.now(),
  };
}

// Re-fetch only details (incl. reviews) for one existing spot.
async function refreshSpotDetails({ apiKey, placeId, cat }) {
  const google = await loadGoogleMaps(apiKey);
  const detail = await placeDetails(google, placeId);
  return placeToSpot(detail, cat);
}

function haversine(lat1, lng1, lat2, lng2) {
  const R = 6371;
  const toRad = (d) => d * Math.PI / 180;
  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  const a = Math.sin(dLat / 2) ** 2 +
            Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(a));
}

// Top-level helper used by the import modal.
async function searchAndConvert({ apiKey, query, cat, radius = 800, categories, maxResults = 20 }) {
  const google = await loadGoogleMaps(apiKey);
  // Look up category-specific search keyword from the live categories list
  // (which the user can edit) before falling back to defaults. We intentionally
  // do NOT apply a Place type filter — type='restaurant' etc. excludes valid
  // matches whose Google classification differs (food-only, bar-only, etc.),
  // and the keyword alone is already restrictive enough.
  const fromList = (categories || []).find((c) => c.id === cat);
  const def = CAT_QUERY[cat] || {};
  const cap = Math.max(1, Math.min(60, Number(maxResults) || 20));

  // Build the keyword list. Each Google textSearch call is capped at ~20
  // results, so to get more we fan out into several related queries.
  // Every query is prefixed with the area name (五反田) so Google ranks
  // local results highest.
  let baseTerms;
  const userQuery = (query || '').trim();
  if (userQuery) {
    // User typed something specific: combine that with each category keyword
    // for fan-out, or use it alone if no category constraint.
    const variants = new Set([userQuery]);
    if (cat && cat !== 'all') {
      const catKws = CAT_KEYWORDS[cat] || (def.keyword ? [def.keyword] : []);
      for (const k of catKws) {
        if (!userQuery.includes(k) && !k.includes(userQuery)) {
          variants.add(userQuery + ' ' + k);
        }
      }
    }
    baseTerms = Array.from(variants).slice(0, 10);
  } else {
    const customKw = (fromList?.keyword || '').trim();
    const defaults = CAT_KEYWORDS[cat] || (def.keyword ? [def.keyword] : ['']);
    baseTerms = customKw
      ? [customKw, ...defaults.filter((k) => k !== customKw)]
      : defaults.slice();
  }

  // Prefix every base term with the area name unless the term already contains it.
  const area = GOTANDA_AREA_NAME;
  const keywords = baseTerms.map((t) =>
    t.includes(area) ? t : `${area} ${t}`
  );

  const center = new google.maps.LatLng(GOTANDA_LATLNG.lat, GOTANDA_LATLNG.lng);

  // Run searches in parallel. Stop early once we already have enough unique ids,
  // by skipping launches we don't need — but in practice the cost difference is
  // tiny and parallel issue is simpler.
  const perKeyword = await Promise.all(keywords.map(async (kw) => {
    const request = { location: center, radius, query: kw };
    try { return await textSearch(google, request); }
    catch (e) {
      console.warn('[places] textSearch failed for keyword', kw, e?.message || e);
      return [];
    }
  }));
  console.log('[places] keywords:', keywords, 'per-keyword counts:', perKeyword.map((r) => r.length));

  // Dedupe by place_id, preserving insertion order. Also drop anything outside
  // the requested radius (Google's `radius` is only a bias for textSearch, so
  // results past it leak in unless we filter ourselves).
  const radiusKm = (Number(radius) || 800) / 1000;
  const seen = new Set();
  const unique = [];
  let droppedFar = 0;
  for (const list of perKeyword) {
    for (const r of list) {
      if (!r?.place_id || seen.has(r.place_id)) continue;
      const loc = r.geometry?.location;
      const lat = typeof loc?.lat === 'function' ? loc.lat() : loc?.lat;
      const lng = typeof loc?.lng === 'function' ? loc.lng() : loc?.lng;
      if (typeof lat === 'number' && typeof lng === 'number') {
        const d = haversine(GOTANDA_LATLNG.lat, GOTANDA_LATLNG.lng, lat, lng);
        if (d > radiusKm) { droppedFar++; continue; }
      }
      seen.add(r.place_id);
      unique.push(r);
      if (unique.length >= cap) break;
    }
    if (unique.length >= cap) break;
  }
  console.log('[places] unique candidates:', unique.length, '(cap', cap + ', dropped', droppedFar, 'beyond', radiusKm + 'km)');

  // Hydrate each result with Place Details (photos require detail call).
  const detailed = await Promise.all(unique.map(async (r) => {
    try {
      const d = await placeDetails(google, r.place_id);
      return placeToSpot({ ...r, ...d }, cat === 'all' ? null : cat);
    } catch {
      return placeToSpot(r, cat === 'all' ? null : cat);
    }
  }));
  return detailed.filter((s) => s && s.id);
}

// ── Google Places UI panel ─────────────────────────────────────────
function GooglePlacesPanel({ lang, onApply, embedded, categories }) {
  const T = (ja, en) => (lang === 'ja' ? ja : en);
  const [apiKey, setApiKey] = usePlaceState(() => localStorage.getItem(GOOGLE_KEY_LS) || '');
  const [query, setQuery] = usePlaceState('');
  const cats = (categories || []).filter((c) => c.id !== 'all');
  const [cat, setCat] = usePlaceState(cats[0]?.id || 'izakaya');
  const [radius, setRadius] = usePlaceState(800);
  const [maxResults, setMaxResults] = usePlaceState(20);
  const [busy, setBusy] = usePlaceState(false);
  const [error, setError] = usePlaceState('');
  const [results, setResults] = usePlaceState([]);
  const [picked, setPicked] = usePlaceState(new Set());

  usePlaceEffect(() => {
    if (apiKey) localStorage.setItem(GOOGLE_KEY_LS, apiKey);
  }, [apiKey]);

  const run = async () => {
    setError(''); setResults([]); setPicked(new Set());
    if (!apiKey.trim()) { setError(T('APIキーを入力してください', 'Please enter an API key')); return; }
    setBusy(true);
    try {
      const spots = await searchAndConvert({
        apiKey: apiKey.trim(), query: query.trim(),
        cat, radius: Number(radius) || 800,
        categories,
        maxResults: Number(maxResults) || 20,
      });
      if (!spots.length) setError(T('結果がありません', 'No results'));
      else {
        setResults(spots);
        setPicked(new Set(spots.map((s) => s.id))); // pre-select all
      }
    } catch (e) {
      setError(String(e.message || e));
    } finally {
      setBusy(false);
    }
  };

  const toggle = (id) => {
    setPicked((s) => {
      const n = new Set(s);
      if (n.has(id)) n.delete(id); else n.add(id);
      return n;
    });
  };
  const selectAll = () => setPicked(new Set(results.map((s) => s.id)));
  const selectNone = () => setPicked(new Set());

  const apply = (mode) => {
    const chosen = results.filter((s) => picked.has(s.id));
    if (!chosen.length) { setError(T('追加するスポットを選択してください', 'Select at least one spot')); return; }
    onApply(chosen, mode);
  };

  return (
    <div className={embedded ? 'gp-panel gp-embedded' : 'gp-panel'}>
      <div className="gp-row">
        <label className="gp-lbl">
          {T('Google Maps APIキー', 'Google Maps API key')}
          <a
            href="https://developers.google.com/maps/documentation/javascript/get-api-key"
            target="_blank" rel="noreferrer"
            className="gp-help"
          >?</a>
        </label>
        <input
          className="gp-input"
          type="password"
          autoComplete="off"
          placeholder="AIza..."
          value={apiKey}
          onChange={(e) => setApiKey(e.target.value)}
        />
        <div className="gp-hint">
          {T(
            'Places API (New) を有効化したキーが必要です。HTTP リファラ制限の設定を推奨。',
            'Requires a key with Places API enabled. HTTP referrer restriction recommended.'
          )}
        </div>
      </div>

      <div className="gp-row gp-grid">
        <div>
          <label className="gp-lbl">{T('カテゴリ', 'Category')}</label>
          <select className="gp-input" value={cat} onChange={(e) => setCat(e.target.value)}>
            {cats.map((c) => (
              <option key={c.id} value={c.id}>
                {c.icon} {c.label?.[lang] || c.id}
              </option>
            ))}
          </select>
        </div>
        <div>
          <label className="gp-lbl">{T('検索半径(m)', 'Radius (m)')}</label>
          <input className="gp-input" type="number" min="200" max="3000" step="100"
                 value={radius} onChange={(e) => setRadius(e.target.value)} />
        </div>
        <div>
          <label className="gp-lbl">{T('件数 (最大60)', 'Max results (60)')}</label>
          <input className="gp-input" type="number" min="1" max="60" step="1"
                 value={maxResults} onChange={(e) => setMaxResults(e.target.value)} />
        </div>
      </div>

      <div className="gp-row">
        <label className="gp-lbl">{T('追加キーワード（任意）', 'Extra keyword (optional)')}</label>
        <input className="gp-input" type="text"
               placeholder={T('例: 五反田 焼き鳥', 'e.g. Gotanda yakitori')}
               value={query} onChange={(e) => setQuery(e.target.value)} />
      </div>

      <div className="imp-actions-row" style={{ marginTop: 4 }}>
        <button className="btn-primary" onClick={run} disabled={busy} style={{ flex: 'none' }}>
          {busy ? T('検索中…', 'Searching…') : T('Googleで検索', 'Search Google')}
        </button>
      </div>

      {error && <div className="imp-msg imp-err">⚠ {error}</div>}

      {results.length > 0 && (
        <>
          <div className="gp-result-head">
            <div>{T(`${results.length}件 / 選択 ${picked.size}件`, `${results.length} found / ${picked.size} selected`)}</div>
            <div style={{ display: 'flex', gap: 12 }}>
              <button className="imp-link" onClick={selectAll}>{T('全選択', 'All')}</button>
              <button className="imp-link" onClick={selectNone}>{T('解除', 'None')}</button>
            </div>
          </div>
          <div className="gp-results">
            {results.map((s) => (
              <label key={s.id} className={`gp-result ${picked.has(s.id) ? 'is-on' : ''}`}>
                <input type="checkbox" checked={picked.has(s.id)} onChange={() => toggle(s.id)} />
                <div className="gp-result-thumb">
                  {s.image ? <img src={s.image} alt="" loading="lazy" /> : <span>—</span>}
                </div>
                <div className="gp-result-body">
                  <div className="gp-result-name">{s.name.ja}</div>
                  <div className="gp-result-meta">
                    <span>★ {s.rating}</span>
                    <span>·</span>
                    <span>{s.price}</span>
                    <span>·</span>
                    <span>{s.walk}{T('分', 'min')}</span>
                  </div>
                  <div className="gp-result-addr">{s.address.ja}</div>
                </div>
              </label>
            ))}
          </div>
          <div className="modal-actions" style={{ padding: 0, marginTop: 12 }}>
            <button className="btn-primary" onClick={() => apply('replace')}>
              {T('置換して適用', 'Replace & apply')}
            </button>
            <button className="btn-secondary" onClick={() => apply('append')}>
              {T('追加', 'Append')}
            </button>
          </div>
          <div className="gp-attrib">Powered by Google</div>
        </>
      )}
    </div>
  );
}

Object.assign(window, { GooglePlacesPanel, searchAndConvert, loadGoogleMaps, refreshSpotDetails });
