// Select — выпадающий список вместо системного <select>.
//
// Нативный select нельзя оформить: браузер рисует его список средствами ОС,
// и на тёмной теме сайта он выпадает светлым системным меню. Здесь список —
// обычная разметка, поэтому подчиняется тем же переменным, что и остальные поля.
//
// Клавиатура работает как у нативного: стрелки двигают подсветку, Enter
// выбирает, Escape закрывает, Home/End прыгают к краям. Для длинных списков
// (часовые пояса) включается поиск через searchable.

function Select({
  value,
  onChange,
  options,          // [{value, label, hint}] или ['a','b']
  placeholder = 'Выберите',
  searchable = false,
  disabled = false,
  className = '',
  style,
  id,
}) {
  const norm = React.useMemo(
    () => (options || []).map(o => (typeof o === 'string' ? { value: o, label: o } : o)),
    [options]
  );
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState('');
  const [active, setActive] = React.useState(-1);
  const rootRef = React.useRef(null);
  const listRef = React.useRef(null);
  const searchRef = React.useRef(null);

  const selected = norm.find(o => String(o.value) === String(value));

  const visible = React.useMemo(() => {
    if (!searchable || !query.trim()) return norm;
    const q = query.trim().toLowerCase();
    return norm.filter(o =>
      o.label.toLowerCase().includes(q) ||
      String(o.value).toLowerCase().includes(q) ||
      (o.hint || '').toLowerCase().includes(q)
    );
  }, [norm, query, searchable]);

  // Клик мимо и уход фокуса за пределы компонента закрывают список.
  React.useEffect(() => {
    if (!open) return;
    const onDocDown = (e) => {
      if (rootRef.current && !rootRef.current.contains(e.target)) close();
    };
    document.addEventListener('mousedown', onDocDown);
    return () => document.removeEventListener('mousedown', onDocDown);
  }, [open]);

  // При открытии подсвечиваем текущий пункт и фокусируем поиск.
  React.useEffect(() => {
    if (!open) return;
    const idx = visible.findIndex(o => String(o.value) === String(value));
    setActive(idx >= 0 ? idx : 0);
    if (searchable && searchRef.current) searchRef.current.focus();
  }, [open]);

  // Держим подсвеченный пункт в поле зрения при навигации с клавиатуры.
  React.useEffect(() => {
    if (!open || active < 0 || !listRef.current) return;
    const el = listRef.current.children[active];
    if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
  }, [active, open]);

  function close() {
    setOpen(false);
    setQuery('');
    setActive(-1);
  }

  function pick(opt) {
    if (!opt) return;
    onChange(opt.value);
    close();
    // Возвращаем фокус на кнопку: иначе после выбора он улетает в body
    // и следующий Tab начинает обход страницы заново.
    if (rootRef.current) {
      const btn = rootRef.current.querySelector('.ui-select-trigger');
      if (btn) btn.focus();
    }
  }

  function onKeyDown(e) {
    if (disabled) return;
    if (!open) {
      if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
        e.preventDefault();
        setOpen(true);
      }
      return;
    }
    switch (e.key) {
      case 'Escape':
        e.preventDefault(); close(); break;
      case 'Enter':
        e.preventDefault(); pick(visible[active]); break;
      case 'ArrowDown':
        e.preventDefault(); setActive(a => Math.min(a + 1, visible.length - 1)); break;
      case 'ArrowUp':
        e.preventDefault(); setActive(a => Math.max(a - 1, 0)); break;
      case 'Home':
        e.preventDefault(); setActive(0); break;
      case 'End':
        e.preventDefault(); setActive(visible.length - 1); break;
      case 'Tab':
        close(); break;
      default:
        break;
    }
  }

  const listID = id ? id + '-list' : undefined;

  return (
    <div
      className={'ui-select' + (open ? ' open' : '') + (disabled ? ' disabled' : '') + (className ? ' ' + className : '')}
      style={style}
      ref={rootRef}
      onKeyDown={onKeyDown}
    >
      <button
        type="button"
        id={id}
        className="ui-select-trigger"
        disabled={disabled}
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-controls={listID}
        onClick={() => !disabled && (open ? close() : setOpen(true))}
      >
        <span className={'ui-select-value' + (selected ? '' : ' placeholder')}>
          {selected ? selected.label : placeholder}
        </span>
        <IconChevDown size={14} className="ui-select-chev" />
      </button>

      {open && (
        <div className="ui-select-menu">
          {searchable && (
            <div className="ui-select-search">
              <IconSearch size={13} />
              <input
                ref={searchRef}
                value={query}
                placeholder="Поиск"
                onChange={e => { setQuery(e.target.value); setActive(0); }}
              />
            </div>
          )}
          <div className="ui-select-list" role="listbox" id={listID} ref={listRef}>
            {visible.length === 0 && (
              <div className="ui-select-empty">Ничего не найдено</div>
            )}
            {visible.map((o, i) => {
              const isSel = String(o.value) === String(value);
              return (
                <div
                  key={String(o.value)}
                  role="option"
                  aria-selected={isSel}
                  className={'ui-select-option' + (i === active ? ' active' : '') + (isSel ? ' selected' : '')}
                  onMouseEnter={() => setActive(i)}
                  // mousedown, а не click: click сработал бы после того, как
                  // обработчик закрытия по клику-мимо уже свернул список.
                  onMouseDown={(e) => { e.preventDefault(); pick(o); }}
                >
                  <span className="ui-select-option-label">{o.label}</span>
                  {o.hint && <span className="ui-select-option-hint">{o.hint}</span>}
                  {isSel && <IconCheck size={13} className="ui-select-tick" />}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// Часовые пояса России и ближнего зарубежья — то, откуда приходит аудитория.
// Список намеренно короткий: полный tzdata (600+ зон) в выпадашке
// неюзабелен, а редкий случай закрывается поиском по подписи.
const TIMEZONES = [
  { value: 'Europe/Kaliningrad', label: 'Калининград', hint: 'UTC+2' },
  { value: 'Europe/Moscow', label: 'Москва, Санкт-Петербург', hint: 'UTC+3' },
  { value: 'Europe/Volgograd', label: 'Волгоград', hint: 'UTC+3' },
  { value: 'Europe/Kirov', label: 'Киров', hint: 'UTC+3' },
  { value: 'Europe/Astrakhan', label: 'Астрахань', hint: 'UTC+4' },
  { value: 'Europe/Samara', label: 'Самара, Ижевск', hint: 'UTC+4' },
  { value: 'Europe/Saratov', label: 'Саратов', hint: 'UTC+4' },
  { value: 'Europe/Ulyanovsk', label: 'Ульяновск', hint: 'UTC+4' },
  { value: 'Asia/Yekaterinburg', label: 'Екатеринбург, Пермь, Уфа', hint: 'UTC+5' },
  { value: 'Asia/Omsk', label: 'Омск', hint: 'UTC+6' },
  { value: 'Asia/Novosibirsk', label: 'Новосибирск', hint: 'UTC+7' },
  { value: 'Asia/Barnaul', label: 'Барнаул', hint: 'UTC+7' },
  { value: 'Asia/Tomsk', label: 'Томск', hint: 'UTC+7' },
  { value: 'Asia/Novokuznetsk', label: 'Новокузнецк', hint: 'UTC+7' },
  { value: 'Asia/Krasnoyarsk', label: 'Красноярск', hint: 'UTC+7' },
  { value: 'Asia/Irkutsk', label: 'Иркутск, Улан-Удэ', hint: 'UTC+8' },
  { value: 'Asia/Chita', label: 'Чита', hint: 'UTC+9' },
  { value: 'Asia/Yakutsk', label: 'Якутск', hint: 'UTC+9' },
  { value: 'Asia/Khandyga', label: 'Хандыга', hint: 'UTC+9' },
  { value: 'Asia/Vladivostok', label: 'Владивосток, Хабаровск', hint: 'UTC+10' },
  { value: 'Asia/Ust-Nera', label: 'Усть-Нера', hint: 'UTC+10' },
  { value: 'Asia/Magadan', label: 'Магадан', hint: 'UTC+11' },
  { value: 'Asia/Sakhalin', label: 'Южно-Сахалинск', hint: 'UTC+11' },
  { value: 'Asia/Srednekolymsk', label: 'Среднеколымск', hint: 'UTC+11' },
  { value: 'Asia/Kamchatka', label: 'Петропавловск-Камчатский', hint: 'UTC+12' },
  { value: 'Asia/Anadyr', label: 'Анадырь', hint: 'UTC+12' },
  { value: 'Europe/Minsk', label: 'Минск', hint: 'UTC+3' },
  { value: 'Europe/Kyiv', label: 'Киев', hint: 'UTC+2' },
  { value: 'Asia/Almaty', label: 'Алматы', hint: 'UTC+5' },
  { value: 'Asia/Tashkent', label: 'Ташкент', hint: 'UTC+5' },
  { value: 'Asia/Tbilisi', label: 'Тбилиси', hint: 'UTC+4' },
  { value: 'Asia/Yerevan', label: 'Ереван', hint: 'UTC+4' },
  { value: 'Asia/Baku', label: 'Баку', hint: 'UTC+4' },
  { value: 'Asia/Bishkek', label: 'Бишкек', hint: 'UTC+6' },
  { value: 'Asia/Dushanbe', label: 'Душанбе', hint: 'UTC+5' },
  { value: 'Asia/Ashgabat', label: 'Ашхабад', hint: 'UTC+5' },
  { value: 'Europe/Chisinau', label: 'Кишинёв', hint: 'UTC+2' },
  { value: 'Europe/Riga', label: 'Рига', hint: 'UTC+2' },
  { value: 'Europe/Vilnius', label: 'Вильнюс', hint: 'UTC+2' },
  { value: 'Europe/Tallinn', label: 'Таллин', hint: 'UTC+2' },
  { value: 'Europe/Berlin', label: 'Берлин', hint: 'UTC+1' },
  { value: 'Europe/London', label: 'Лондон', hint: 'UTC+0' },
  { value: 'Asia/Dubai', label: 'Дубай', hint: 'UTC+4' },
  { value: 'Asia/Bangkok', label: 'Бангкок', hint: 'UTC+7' },
  { value: 'America/New_York', label: 'Нью-Йорк', hint: 'UTC−5' },
  { value: 'America/Los_Angeles', label: 'Лос-Анджелес', hint: 'UTC−8' },
  { value: 'UTC', label: 'UTC', hint: 'UTC+0' },
];

// Пол — необязательное поле: он управляет родом обращения в тексте. Пустая
// строка и есть «не указывать», поэтому это полноценный пункт списка, а не
// только плейсхолдер.
//
// «По имени» вместо прежнего «Не указывать»: пустое поле больше не означает
// безродный текст. Сервер угадывает род по имени (llm.GuessGender) и уходит в
// нейтральные формы только на неоднозначных именах — безродный русский на
// десять тысяч слов модель не выдерживает, и разбор уезжал клиенту с чужим
// родом.
const GENDER_OPTIONS = [
  { value: '', label: 'По имени' },
  { value: 'female', label: 'Женский' },
  { value: 'male', label: 'Мужской' },
];

// TimezoneSelect — часовой пояс. Значение, которого нет в коротком списке
// (пришло из геокодера), не теряется: подставляем его отдельным пунктом,
// иначе выбор молча схлопнулся бы в плейсхолдер.
function TimezoneSelect({ value, onChange, id, className, style, disabled }) {
  const options = React.useMemo(() => {
    if (!value || TIMEZONES.some(t => t.value === value)) return TIMEZONES;
    return [{ value, label: value, hint: 'определён по месту рождения' }, ...TIMEZONES];
  }, [value]);
  return (
    <Select
      id={id}
      className={className}
      style={style}
      value={value}
      onChange={onChange}
      options={options}
      disabled={disabled}
      searchable
      placeholder="Часовой пояс"
    />
  );
}

// PlaceField — поле «место рождения» с автокомплитом от геокодера. Юзер выбирает
// конкретный город (одноимённые различаются) → onPick фиксирует lat/lng/tz, и
// бэкенд не геокодит свободный текст заново (без расползания данных).
const PlaceField = ({ value, onChange, onPick }) => {
  const [sugg, setSugg] = React.useState([]);
  const [open, setOpen] = React.useState(false);
  const tRef = React.useRef(null);
  const fetchSugg = (q) => {
    clearTimeout(tRef.current);
    if ((q || '').trim().length < 3) { setSugg([]); setOpen(false); return; }
    tRef.current = setTimeout(async () => {
      try {
        const r = await fetch('/api/geocode/suggest?q=' + encodeURIComponent(q), { credentials: 'same-origin' });
        const d = await r.json();
        setSugg(d.suggestions || []); setOpen((d.suggestions || []).length > 0);
      } catch (_) { setSugg([]); }
    }, 300);
  };
  return (
    <div style={{ position: 'relative' }}>
      <input className="input" value={value} placeholder="Начни вводить город…"
        onChange={e => { onChange(e.target.value); fetchSugg(e.target.value); }}
        onFocus={() => sugg.length && setOpen(true)}
        onBlur={() => setTimeout(() => setOpen(false), 150)} />
      {open && sugg.length > 0 && (
        <div className="card" style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 30, maxHeight: 220, overflowY: 'auto', background: 'var(--bg-raised)', marginTop: 4, padding: 4, boxShadow: '0 8px 24px rgba(0,0,0,0.18)' }}>
          {sugg.map((s, i) => (
            <div key={i} onMouseDown={() => { onPick(s); setOpen(false); setSugg([]); }}
              style={{ padding: '8px 10px', cursor: 'pointer', fontSize: 13, borderRadius: 6, color: 'var(--ink-secondary)' }}
              onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-base)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
              {s.name}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

// DELIVERY_HOURS — во сколько присылать прогноз.
//
// Сутки целиком, а не «удобные часы»: кто-то читает перед сном, кто-то в ночную
// смену. Подпись говорит, за какой день будет текст, потому что это не
// очевидно: прогноз всегда про те сутки, в которые он приходит, и выбравший
// одиннадцать вечера получит текст про уходящий день, а не про завтрашний.
const DELIVERY_HOURS = Array.from({ length: 24 }, (_, h) => ({
  value: String(h),
  label: String(h).padStart(2, '0') + ':00',
  hint: h < 5 ? 'ночью' : h < 12 ? 'утром' : h < 18 ? 'днём' : 'вечером',
}));

// browserTZ — пояс, который сообщает браузер. Нужен, чтобы не заставлять
// человека искать свой город в списке: подставляем и даём поменять.
function browserTZ() {
  try {
    return Intl.DateTimeFormat().resolvedOptions().timeZone || '';
  } catch (_) {
    return '';
  }
}

// DeliveryPicker — когда присылать ежедневный прогноз.
//
// Пояс и час рядом намеренно: по отдельности они бессмысленны. «Девять утра» —
// это девять утра где, а «Новосибирск» — это во сколько?
function DeliveryPicker({ tz, hour, onTZ, onHour, disabled, note }) {
  return (
    <div style={{ marginBottom: '20px', maxWidth: '560px' }}>
      <span className="label" style={{ display: 'block', marginBottom: '8px' }}>Когда присылать</span>
      <div className="rgrid-2" style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: '10px' }}>
        <TimezoneSelect value={tz} onChange={onTZ} disabled={disabled} />
        <Select value={String(hour)} onChange={v => onHour(Number(v))}
          options={DELIVERY_HOURS} placeholder="Час" disabled={disabled} />
      </div>
      <span className="mono muted" style={{ fontSize: '10.5px', display: 'block', marginTop: '6px', lineHeight: 1.6 }}>
        {note || 'Прогноз придёт в это время по твоим часам — и будет про те сутки, в которые пришёл.'}
      </span>
    </div>
  );
}

Object.assign(window, {
  Select, TimezoneSelect, TIMEZONES, GENDER_OPTIONS, PlaceField,
  DeliveryPicker, DELIVERY_HOURS, browserTZ,
});

