import React, { useState, useEffect, useRef, useCallback } from 'react'
import { MovieSite } from './Settings'

const Select: React.FC<{
  value: string
  onChange: (v: string) => void
  options: { value: string; label: string }[]
  placeholder?: string
}> = ({ value, onChange, options, placeholder }) => {
  const [open, setOpen] = useState(false)
  const ref = useRef<HTMLDivElement>(null)
  const selected = options.find(o => o.value === value)

  useEffect(() => {
    if (!open) return
    const handler = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [open])

  return (
    <div ref={ref} className={`ms-select${open ? ' open' : ''}`} onClick={() => setOpen(o => !o)}>
      <div className="ms-select-trigger">
        <span className={selected && selected.value ? 'ms-select-active' : 'ms-select-placeholder'}>
          {selected ? selected.label : (placeholder ?? '')}
        </span>
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="6 9 12 15 18 9" />
        </svg>
      </div>
      {open && (
        <div className="ms-select-dropdown" onClick={e => e.stopPropagation()}>
          {options.map(o => (
            <div
              key={o.value}
              className={`ms-select-opt${o.value === value ? ' active' : ''}`}
              onClick={() => { onChange(o.value); setOpen(false) }}
            >{o.label}</div>
          ))}
        </div>
      )}
    </div>
  )
}

// Searchable single-select dropdown — для длинных списков (тематик).
const SearchableSelect: React.FC<{
  value: string
  onChange: (v: string) => void
  options: { value: string; label: string }[]
  placeholder: string
  maxHeight?: number
}> = ({ value, onChange, options, placeholder, maxHeight = 320 }) => {
  const [open, setOpen] = useState(false)
  const [search, setSearch] = useState('')
  const ref = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const selected = options.find(o => o.value === value)

  useEffect(() => {
    if (!open) return
    const handler = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); setSearch('') }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [open])

  useEffect(() => { if (open) requestAnimationFrame(() => inputRef.current?.focus()) }, [open])

  const q = search.trim().toLowerCase()
  const filtered = q ? options.filter(o => o.label.toLowerCase().includes(q)) : options

  return (
    <div ref={ref} className={`ms-select ms-searchable${open ? ' open' : ''}`}>
      <div className="ms-select-trigger" onClick={() => setOpen(o => !o)}>
        <span className={selected ? 'ms-select-active' : 'ms-select-placeholder'}>
          {selected ? selected.label : placeholder}
        </span>
        {selected && (
          <button className="ms-multi-clear" onClick={e => { e.stopPropagation(); onChange('') }} title="Сбросить">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
            </svg>
          </button>
        )}
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="6 9 12 15 18 9" />
        </svg>
      </div>
      {open && (
        <div className="ms-select-dropdown ms-searchable-dropdown" onClick={e => e.stopPropagation()}>
          <div className="ms-searchable-input-wrap">
            <svg className="ms-searchable-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
            </svg>
            <input
              ref={inputRef}
              className="ms-searchable-input"
              placeholder="Найти тему..."
              value={search}
              onChange={e => setSearch(e.target.value)}
            />
          </div>
          <div className="ms-searchable-list" style={{ maxHeight }}>
            {filtered.map(o => (
              <div
                key={o.value}
                className={`ms-select-opt${o.value === value ? ' active' : ''}`}
                onClick={() => { onChange(o.value); setOpen(false); setSearch('') }}
              >{o.label}</div>
            ))}
            {!filtered.length && <div className="ms-searchable-empty">Ничего не найдено</div>}
          </div>
        </div>
      )}
    </div>
  )
}

// Multi-select dropdown with checkboxes. Trigger shows N selected or placeholder.
const MultiSelect: React.FC<{
  values: string[]
  onChange: (v: string[]) => void
  options: { value: string; label: string }[]
  placeholder: string
  maxHeight?: number
}> = ({ values, onChange, options, placeholder, maxHeight = 260 }) => {
  const [open, setOpen] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!open) return
    const handler = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [open])

  const toggle = (v: string) => {
    onChange(values.includes(v) ? values.filter(x => x !== v) : [...values, v])
  }

  const label = values.length === 0
    ? placeholder
    : values.length === 1
      ? (options.find(o => o.value === values[0])?.label ?? values[0])
      : `${placeholder}: ${values.length}`

  return (
    <div ref={ref} className={`ms-select${open ? ' open' : ''}`}>
      <div className="ms-select-trigger" onClick={() => setOpen(o => !o)}>
        <span className={values.length > 0 ? 'ms-select-active' : 'ms-select-placeholder'}>{label}</span>
        {values.length > 0 && (
          <button className="ms-multi-clear" onClick={e => { e.stopPropagation(); onChange([]) }} title="Сбросить">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
            </svg>
          </button>
        )}
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="6 9 12 15 18 9" />
        </svg>
      </div>
      {open && (
        <div className="ms-select-dropdown ms-multi-dropdown" style={{ maxHeight }} onClick={e => e.stopPropagation()}>
          {options.map(o => {
            const on = values.includes(o.value)
            return (
              <div key={o.value} className={`ms-select-opt ms-multi-opt${on ? ' active' : ''}`} onClick={() => toggle(o.value)}>
                <span className={`ms-checkbox${on ? ' on' : ''}`}>{on && (
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
                    <polyline points="20 6 9 17 4 12" />
                  </svg>
                )}</span>
                {o.label}
              </div>
            )
          })}
        </div>
      )}
    </div>
  )
}

interface TmdbMovie {
  id: number
  mediaType: 'movie' | 'tv'
  title: string
  originalTitle: string
  year: string
  poster: string
  overview: string
  rating: string
}

interface SiteResult {
  title: string
  url: string
  poster?: string
  year?: string
  source: string
}

interface MovieSearchProps {
  onOpenUrl: (name: string, url: string) => void
  onBookmark?: (title: string, url: string, poster: string, source: string) => void
  initialQuery?: string
}

const MOVIE_GENRES = [
  { id: 28, name: 'Боевик' }, { id: 12, name: 'Приключения' }, { id: 16, name: 'Мультфильм' },
  { id: 35, name: 'Комедия' }, { id: 80, name: 'Криминал' }, { id: 99, name: 'Документальный' },
  { id: 18, name: 'Драма' }, { id: 10751, name: 'Семейный' }, { id: 14, name: 'Фэнтези' },
  { id: 36, name: 'История' }, { id: 27, name: 'Ужасы' }, { id: 9648, name: 'Детектив' },
  { id: 10749, name: 'Мелодрама' }, { id: 878, name: 'Фантастика' }, { id: 53, name: 'Триллер' },
  { id: 10752, name: 'Военный' }, { id: 37, name: 'Вестерн' },
]

const TV_GENRES = [
  { id: 10759, name: 'Боевик' }, { id: 16, name: 'Мультфильм' }, { id: 35, name: 'Комедия' },
  { id: 80, name: 'Криминал' }, { id: 99, name: 'Документальный' }, { id: 18, name: 'Драма' },
  { id: 10751, name: 'Семейный' }, { id: 10762, name: 'Детское' }, { id: 9648, name: 'Детектив' },
  { id: 10765, name: 'Фантастика' }, { id: 10768, name: 'Политика' }, { id: 37, name: 'Вестерн' },
]

// Кураторский список тематик. Каждая — pipe-OR по keyword ID'ам из TMDB.
// extraGenre добавляется AND-связкой к выбранным жанрам (только в movie-режиме),
// если тематика требует фильтра по жанру для чистоты выдачи (например, "Музыка"
// без жанра 10402 даёт хорроры со скрипачами в кадре).
// IDs провалидированы probe-скриптом (/search/keyword + /discover/movie):
// каждая тема даёт ≥50 результатов на первой странице популярных.
const THEMES: { id: string; label: string; keywordIds: number[]; extraGenre?: number }[] = [
  { id: 'war',          label: 'Война',                    keywordIds: [1956, 273967] },
  { id: 'coldwar',      label: 'Холодная война',           keywordIds: [2106] },
  { id: 'vietnam',      label: 'Вьетнамская война',        keywordIds: [2957] },
  { id: 'zombie',       label: 'Зомби',                    keywordIds: [12377] },
  { id: 'vampire',      label: 'Вампиры',                  keywordIds: [3133] },
  { id: 'ghost',        label: 'Призраки',                 keywordIds: [162846] },
  { id: 'witch',        label: 'Ведьмы',                   keywordIds: [616] },
  { id: 'magic',        label: 'Маги и волшебство',        keywordIds: [2343, 177912] },
  { id: 'dragon',       label: 'Драконы',                  keywordIds: [12554] },
  { id: 'fairytale',    label: 'Сказки',                   keywordIds: [3205] },
  { id: 'cannibal',     label: 'Каннибалы',                keywordIds: [278235, 14895] },
  { id: 'cult',         label: 'Секты',                    keywordIds: [6158] },
  { id: 'serialkiller', label: 'Серийные убийцы',          keywordIds: [10714] },
  { id: 'space',        label: 'Космос',                   keywordIds: [252634, 3801, 4344] },
  { id: 'alien',        label: 'Инопланетяне',             keywordIds: [9951] },
  { id: 'timetravel',   label: 'Путешествия во времени',   keywordIds: [4379] },
  { id: 'postapoc',     label: 'Постапокалипсис',          keywordIds: [4458, 359337] },
  { id: 'dystopia',     label: 'Антиутопия',               keywordIds: [4565, 350338] },
  { id: 'epidemic',     label: 'Эпидемия и вирус',         keywordIds: [188973, 17995, 188957] },
  { id: 'disaster',     label: 'Стихийные бедствия',       keywordIds: [5096, 10617] },
  { id: 'dinosaur',     label: 'Динозавры',                keywordIds: [12616] },
  { id: 'robot',        label: 'Роботы и ИИ',              keywordIds: [14544, 803, 371846] },
  { id: 'superhero',    label: 'Супергерои',               keywordIds: [9715], extraGenre: 28 },
  { id: 'spy',          label: 'Шпионы',                   keywordIds: [470, 4289] },
  { id: 'mafia',        label: 'Мафия',                    keywordIds: [10391, 10291] },
  { id: 'gangster',     label: 'Гангстеры',                keywordIds: [3149] },
  { id: 'heist',        label: 'Ограбления',               keywordIds: [10051, 642] },
  { id: 'prison',       label: 'Тюрьма',                   keywordIds: [378] },
  { id: 'escape',       label: 'Побег из тюрьмы',          keywordIds: [9777] },
  { id: 'police',       label: 'Полиция и детективы',      keywordIds: [703, 6149] },
  { id: 'court',        label: 'Суд и адвокаты',           keywordIds: [33519, 10909] },
  { id: 'journalism',   label: 'Журналисты',               keywordIds: [736, 917] },
  { id: 'doctor',       label: 'Врачи и медицина',         keywordIds: [11612, 13005] },
  { id: 'psych',        label: 'Психбольница',             keywordIds: [11857, 10323] },
  { id: 'school',       label: 'Школа',                    keywordIds: [6270] },
  { id: 'college',      label: 'Колледж и студенты',       keywordIds: [3616] },
  { id: 'martial',      label: 'Боевые искусства',         keywordIds: [779, 780], extraGenre: 28 },
  { id: 'samurai',      label: 'Самураи',                  keywordIds: [1462] },
  { id: 'ninja',        label: 'Ниндзя',                   keywordIds: [10278] },
  { id: 'boxing',       label: 'Бокс',                     keywordIds: [209476] },
  { id: 'football',     label: 'Футбол',                   keywordIds: [13042, 352822] },
  { id: 'sport',        label: 'Спорт (общий)',            keywordIds: [333328] },
  { id: 'olympics',     label: 'Олимпиада',                keywordIds: [315138] },
  { id: 'racing',       label: 'Гонки',                    keywordIds: [191279, 9666] },
  { id: 'skiing',       label: 'Лыжи и сноуборд',          keywordIds: [248915, 3522] },
  { id: 'mountains',    label: 'Горы и альпинизм',         keywordIds: [159212, 160177] },
  { id: 'surfing',      label: 'Сёрфинг',                  keywordIds: [5349] },
  { id: 'chess',        label: 'Шахматы',                  keywordIds: [316] },
  { id: 'submarine',    label: 'Подводные лодки',          keywordIds: [339] },
  { id: 'ship',         label: 'Корабли и море',           keywordIds: [3799, 191585] },
  { id: 'airplane',     label: 'Самолёты и пилоты',        keywordIds: [3800, 3203] },
  { id: 'train',        label: 'Поезда',                   keywordIds: [13008] },
  { id: 'rome',         label: 'Древний Рим',              keywordIds: [5049, 1405] },
  { id: 'medieval',     label: 'Средневековье',            keywordIds: [355987, 161257] },
  { id: 'revolution',   label: 'Революция',                keywordIds: [2020] },
  { id: 'biography',    label: 'Биография и реальные события', keywordIds: [5565, 9672] },
  { id: 'artist',       label: 'Художники и писатели',     keywordIds: [2679, 13028] },
  { id: 'music',        label: 'Музыка и группы',          keywordIds: [18001, 4048], extraGenre: 10402 },
  { id: 'circus',       label: 'Цирк',                     keywordIds: [291] },
  { id: 'cooking',      label: 'Кулинария',                keywordIds: [1918, 18293] },
  { id: 'wallstreet',   label: 'Уолл-стрит и финансы',     keywordIds: [5636, 177493] },
  { id: 'religion',     label: 'Религия',                  keywordIds: [11001] },
  { id: 'afterlife',    label: 'Жизнь после смерти',       keywordIds: [6155, 5484] },
  { id: 'dream',        label: 'Сны',                      keywordIds: [346773] },
  { id: 'love',         label: 'О любви',                  keywordIds: [9673] },
  { id: 'wedding',      label: 'Свадьба',                  keywordIds: [13027] },
  { id: 'friendship',   label: 'Дружба',                   keywordIds: [6054] },
  { id: 'family',       label: 'Семья',                    keywordIds: [10235] },
  { id: 'twins',        label: 'Близнецы',                 keywordIds: [15016] },
  { id: 'africa',       label: 'Африка',                   keywordIds: [409] },
  { id: 'japan',        label: 'Япония',                   keywordIds: [233] },
]

const SORTS = [
  { value: 'popularity.desc', label: 'Популярные' },
  { value: 'vote_average.desc', label: 'По рейтингу' },
  { value: 'release_date.desc', label: 'Новые' },
  { value: 'revenue.desc', label: 'По сборам' },
]

const RATINGS = [
  { value: '', label: 'Любой' },
  { value: '5', label: '5+' },
  { value: '6', label: '6+' },
  { value: '7', label: '7+' },
  { value: '8', label: '8+' },
  { value: '9', label: '9+' },
]

const COUNTRIES = [
  { value: '', label: 'Страна' },
  { value: 'US', label: 'США' },
  { value: 'RU', label: 'Россия' },
  { value: 'GB', label: 'Великобритания' },
  { value: 'FR', label: 'Франция' },
  { value: 'DE', label: 'Германия' },
  { value: 'IT', label: 'Италия' },
  { value: 'ES', label: 'Испания' },
  { value: 'JP', label: 'Япония' },
  { value: 'KR', label: 'Южная Корея' },
  { value: 'CN', label: 'Китай' },
  { value: 'IN', label: 'Индия' },
  { value: 'SE', label: 'Швеция' },
  { value: 'DK', label: 'Дания' },
  { value: 'TR', label: 'Турция' },
]

const CURRENT_YEAR = new Date().getFullYear()
const YEARS = Array.from({ length: CURRENT_YEAR - 1899 }, (_, i) => CURRENT_YEAR + 1 - i)

const StarIcon = () => (
  <svg width="11" height="11" viewBox="0 0 24 24" fill="#f5c518" stroke="none">
    <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
  </svg>
)

const MovieCard: React.FC<{ movie: TmdbMovie; idx: number; baseIdx: number; onSelect: (m: TmdbMovie) => void }> = ({ movie, idx, baseIdx, onSelect }) => (
  <div
    className="movie-result-card"
    style={{ animationDelay: `${Math.max(0, idx - baseIdx) * 30}ms` }}
    onClick={() => onSelect(movie)}
  >
    <div className="movie-result-poster">
      {movie.poster
        ? <img src={movie.poster} alt={movie.title} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style') }} />
        : null
      }
      <div className="movie-result-poster-placeholder" style={movie.poster ? { display: 'none' } : undefined}>
        {movie.title.charAt(0).toUpperCase()}
      </div>
    </div>
    <div className="movie-result-info">
      <span className="movie-result-title">{movie.title}</span>
      <div className="ms-card-meta">
        {movie.year && <span className="movie-result-year">{movie.year}</span>}
        {movie.rating && <span className="ms-card-rating"><StarIcon /> {movie.rating}</span>}
      </div>
      {movie.mediaType === 'tv' && <span className="ms-card-type">Сериал</span>}
    </div>
  </div>
)

const MovieSearch: React.FC<MovieSearchProps> = ({ onOpenUrl, onBookmark, initialQuery = '' }) => {
  const [query, setQuery] = useState(initialQuery)
  const [mediaType, setMediaType] = useState<'movie' | 'tv'>('movie')
  const [sortBy, setSortBy] = useState('popularity.desc')
  const [genreIds, setGenreIds] = useState<number[]>([])
  const [themeId, setThemeId] = useState<string>('') // single-select
  const [years, setYears] = useState<string[]>([])
  const [minRating, setMinRating] = useState('')
  const [countries, setCountries] = useState<string[]>([])
  const [page, setPage] = useState(1)
  const [totalPages, setTotalPages] = useState(1)

  const [activeQuery, setActiveQuery] = useState(initialQuery) // committed query (on Enter)
  const [tmdbResults, setTmdbResults] = useState<TmdbMovie[]>([])
  const [selected, setSelected] = useState<TmdbMovie | null>(null)
  const [siteResults, setSiteResults] = useState<SiteResult[]>([])
  const [tmdbLoading, setTmdbLoading] = useState(false)
  const [sitesLoading, setSitesLoading] = useState(false)
  const [loadingMore, setLoadingMore] = useState(false)
  const [cardBase, setCardBase] = useState(0)
  const [message, setMessage] = useState('')
  const [apiKey, setApiKey] = useState('')
  const [sites, setSites] = useState<MovieSite[]>([])
  const [configLoaded, setConfigLoaded] = useState(false)

  const isSearchMode = activeQuery.trim().length > 0
  const genres = mediaType === 'tv' ? TV_GENRES : MOVIE_GENRES

  useEffect(() => {
    window.electron?.readConfig().then((cfg: any) => {
      let enabled: MovieSite[] = (cfg?.movieSites ?? []).filter((s: MovieSite) => s.enabled !== false)
      const NON_MOVIE = /youtube|rutube|vk\.com|ok\.ru|google|yandex|mail\.ru|twitch|tiktok|instagram|facebook|twitter|telegram/i
      if (!enabled.length && cfg?.apps?.length) {
        enabled = cfg.apps
          .filter((app: any) => { try { return !NON_MOVIE.test(new URL(app.url).hostname) } catch { return false } })
          .map((app: any) => {
            let domain = app.url
            try { domain = new URL(app.url).hostname } catch (_) {}
            const type: MovieSite['type'] = /rezka/.test(domain) ? 'hdrezka' : /filmix/.test(domain) ? 'filmix' : 'dle'
            return { domain, type, enabled: true }
          })
      }
      const key: string = cfg?.tmdbApiKey ?? ''
      setSites(enabled)
      setApiKey(key)
      setConfigLoaded(true)
      if (initialQuery) {
        if (key) doTmdbSearch(initialQuery, key)
        else doSiteSearch(initialQuery, enabled, undefined, undefined)
      }
    })
  }, [])

  // Auto-load discover when config ready or filters change (discover mode only)
  const discoverRef = useRef(0)
  const doDiscover = useCallback(async (key: string, pg: number, append: boolean) => {
    const token = ++discoverRef.current
    if (append) setLoadingMore(true)
    else { setTmdbLoading(true); setMessage('') }
    try {
      const theme = THEMES.find(t => t.id === themeId)
      const themeKeywords = theme?.keywordIds ?? []
      // extraGenre — только для фильмов (TV-каталог TMDB имеет другие genre ID)
      const effectiveGenres = (theme?.extraGenre && mediaType === 'movie' && !genreIds.includes(theme.extraGenre))
        ? [...genreIds, theme.extraGenre]
        : genreIds
      const res = await window.electron!.discoverTmdb({ apiKey: key, mediaType, sortBy, genreIds: effectiveGenres, years, minRating, countries, page: pg, themeKeywords })
      if (token !== discoverRef.current) return
      if (res.error) { setMessage(`Ошибка: ${res.error}`); return }
      setTmdbResults(prev => append ? [...prev, ...res.results] : res.results)
      setTotalPages(res.totalPages)
      if (!res.results.length && !append) setMessage('Ничего не найдено')
    } catch {
      if (token === discoverRef.current) setMessage('Ошибка загрузки')
    } finally {
      if (token === discoverRef.current) { setTmdbLoading(false); setLoadingMore(false) }
    }
  }, [mediaType, sortBy, genreIds, themeId, years, minRating, countries])

  useEffect(() => {
    if (!configLoaded || !apiKey || isSearchMode) return
    setPage(1)
    setTmdbResults([])
    doDiscover(apiKey, 1, false)
  }, [configLoaded, apiKey, mediaType, sortBy, genreIds, themeId, years, minRating, countries, isSearchMode])

  const searchRef = useRef(0)
  const doTmdbSearch = async (q: string, key: string) => {
    const token = ++searchRef.current
    setTmdbLoading(true)
    setMessage('')
    setTmdbResults([])
    setSelected(null)
    setSiteResults([])
    try {
      const res = await window.electron!.searchTmdb(q, key)
      if (token !== searchRef.current) return
      if (res.error) { setMessage(`Ошибка TMDB: ${res.error}`); return }
      setTmdbResults(res.results)
      setTotalPages(1)
      if (!res.results.length) setMessage('Ничего не найдено')
    } catch {
      if (token === searchRef.current) setMessage('Ошибка TMDB')
    } finally {
      if (token === searchRef.current) setTmdbLoading(false)
    }
  }

  // Стриминг: каждый сайт ответил → результаты добавляются немедленно. Спиннер виден до первого ответа.
  const siteSearchCancelRef = useRef<(() => void) | null>(null)
  const doSiteSearch = (q: string, sitesToSearch: MovieSite[], yearHint?: string, mt?: string, append = false): (() => void) | null => {
    if (!sitesToSearch.length) {
      if (!append) setMessage('Нет активных сайтов. Добавьте в Настройки → Поиск фильмов.')
      return null
    }
    // Отменить предыдущий стрим (если был)
    if (!append && siteSearchCancelRef.current) { siteSearchCancelRef.current(); siteSearchCancelRef.current = null }

    if (!append) {
      setSitesLoading(true)
      setMessage('')
      setSiteResults([])
    }

    const y = yearHint ? parseInt(yearHint) : NaN
    const isTv = mt === 'tv'
    const yearDist = (r: SiteResult) => r.year ? Math.abs(parseInt(r.year) - y) : 0.5
    const filterBatch = (batch: SiteResult[]): SiteResult[] => {
      if (!yearHint) return batch
      // Для фильмов выкидываем результаты с явно неверным годом (>1 год отличия).
      // Для сериалов год часто не совпадает (разные сезоны) — оставляем все.
      const filtered = isTv ? batch : batch.filter(r => !r.year || yearDist(r) <= 1)
      return filtered.sort((a, b) => yearDist(a) - yearDist(b))
    }

    const cancel = window.electron!.searchMoviesStream(
      q,
      sitesToSearch,
      ({ source: _src, results }: { source: string; results: SiteResult[] }) => {
        const batch = filterBatch(results)
        if (!batch.length) return
        setSiteResults(prev => {
          // Дедупим по URL (если параллельный поиск по originalTitle принесёт те же ссылки).
          const seen = new Set(prev.map(r => r.url))
          const fresh = batch.filter(r => !seen.has(r.url))
          if (!fresh.length) return prev
          const merged = [...prev, ...fresh]
          return yearHint ? merged.sort((a, b) => yearDist(a) - yearDist(b)) : merged
        })
      },
      () => {
        setSitesLoading(false)
        setSiteResults(prev => {
          if (!prev.length && !append) setMessage('Не найдено ни на одном сайте')
          return prev
        })
      },
    )
    if (!append) siteSearchCancelRef.current = cancel
    return cancel
  }

  // Чистка слушателей при размонтировании
  useEffect(() => () => { if (siteSearchCancelRef.current) siteSearchCancelRef.current() }, [])

  const handleSearch = () => {
    const q = query.trim()
    if (!q) return
    setActiveQuery(q)
    if (apiKey) doTmdbSearch(q, apiKey)
    else doSiteSearch(q, sites, undefined, undefined)
  }

  const handleSelectMovie = (movie: TmdbMovie) => {
    setSelected(movie)
    setSiteResults([])
    setMessage('')
    const searchTitle = movie.title || movie.originalTitle
    doSiteSearch(searchTitle, sites, movie.year, movie.mediaType)
    // Параллельный поиск по оригинальному названию — append=true, чтобы не сбросить первичный поиск.
    // Дедуп по URL уже в doSiteSearch.
    if (movie.originalTitle && movie.originalTitle !== movie.title) {
      doSiteSearch(movie.originalTitle, sites, movie.year, movie.mediaType, true)
    }
  }

  const handleBack = () => { setSelected(null); setSiteResults([]); setMessage('') }

  const handleLoadMore = () => {
    const nextPage = page + 1
    setPage(nextPage)
    setCardBase(tmdbResults.length)
    doDiscover(apiKey, nextPage, true)
  }

  const handleFilterChange = (fn: () => void) => {
    setSelected(null)
    setSiteResults([])
    fn()
  }

  const clearQuery = () => {
    setQuery('')
    setActiveQuery('')
    setTmdbResults([])
    setMessage('')
  }

  const loading = tmdbLoading || sitesLoading


  return (
    <div className="movie-search">

      {/* Search bar */}
      <div className="movie-search-bar">
        <input
          className="header-search-input movie-search-input"
          placeholder="Название фильма или сериала..."
          value={query}
          onChange={e => setQuery(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && !loading && handleSearch()}
          autoFocus
        />
        {query && (
          <button className="ms-clear-btn" onClick={clearQuery} title="Очистить">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
            </svg>
          </button>
        )}
        <button className="header-search-btn movie-search-btn" onClick={handleSearch} disabled={loading}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
          </svg>
        </button>
      </div>

      {/* Filters (only in discover mode with TMDB key) */}
      {apiKey && !selected && (
        <div className="ms-filters">
          <div className="ms-filter-row">
            {/* Type toggle */}
            <div className="ms-type-toggle">
              <button
                className={`ms-type-btn${mediaType === 'movie' ? ' active' : ''}`}
                onClick={() => handleFilterChange(() => { setMediaType('movie'); setGenreIds([]); setThemeId('') })}
              >Фильмы</button>
              <button
                className={`ms-type-btn${mediaType === 'tv' ? ' active' : ''}`}
                onClick={() => handleFilterChange(() => { setMediaType('tv'); setGenreIds([]); setThemeId('') })}
              >Сериалы</button>
            </div>

            {/* Sort */}
            {!isSearchMode && (
              <Select value={sortBy} onChange={v => handleFilterChange(() => setSortBy(v))} options={SORTS} />
            )}

            {/* Min rating (single, threshold) */}
            {!isSearchMode && (
              <Select
                value={minRating}
                onChange={v => handleFilterChange(() => setMinRating(v))}
                options={RATINGS.map(r => ({ value: r.value, label: r.label === 'Любой' ? 'Рейтинг' : r.label }))}
                placeholder="Рейтинг"
              />
            )}

            {/* Years (multi, OR) */}
            <MultiSelect
              values={years}
              onChange={v => handleFilterChange(() => setYears(v))}
              options={YEARS.map(y => ({ value: String(y), label: String(y) }))}
              placeholder="Год"
            />

            {/* Countries (multi, OR) */}
            {!isSearchMode && (
              <MultiSelect
                values={countries}
                onChange={v => handleFilterChange(() => setCountries(v))}
                options={COUNTRIES.filter(c => c.value).map(c => ({ value: c.value, label: c.label }))}
                placeholder="Страна"
              />
            )}
          </div>

          {/* Themes — searchable dropdown (71 тематика, длинный список) */}
          {!isSearchMode && (
            <div className="ms-theme-row">
              <SearchableSelect
                value={themeId}
                onChange={v => handleFilterChange(() => setThemeId(v))}
                options={THEMES.map(t => ({ value: t.id, label: t.label }))}
                placeholder="Тематика (например: о войне, зомби, шахматы)"
              />
            </div>
          )}

          {/* Genres (multi, AND — фильм должен соответствовать ВСЕМ выбранным жанрам) */}
          {!isSearchMode && (
            <div className="ms-genres">
              <button
                className={`ms-genre-chip${genreIds.length === 0 ? ' active' : ''}`}
                onClick={() => handleFilterChange(() => setGenreIds([]))}
                title="Сбросить жанры"
              >Все</button>
              {genres.map(g => (
                <button
                  key={g.id}
                  className={`ms-genre-chip${genreIds.includes(g.id) ? ' active' : ''}`}
                  onClick={() => handleFilterChange(() =>
                    setGenreIds(prev => prev.includes(g.id) ? prev.filter(x => x !== g.id) : [...prev, g.id])
                  )}
                >{g.name}</button>
              ))}
              {genreIds.length > 1 && (
                <span className="ms-genres-hint">все выбранные одновременно</span>
              )}
            </div>
          )}
        </div>
      )}

      {message && <p className="movie-search-message">{message}</p>}

      {/* Detail view */}
      {selected ? (
        <div className="ms-detail">
          {selected.poster && (
            <div className="ms-detail-bg">
              <img className="ms-detail-bg-img" src={selected.poster} alt="" />
              <div className="ms-detail-bg-gradient" />
            </div>
          )}
          <div className="ms-detail-content">
            <button className="ms-back-btn" onClick={handleBack}>← Назад</button>
            <div className="ms-detail-card">
              {selected.poster
                ? <img className="ms-detail-poster" src={selected.poster} alt={selected.title} />
                : <div className="ms-poster-placeholder">{selected.title.charAt(0)}</div>
              }
              <div className="ms-detail-info">
                <h2 className="ms-detail-title">{selected.title}</h2>
                {selected.originalTitle !== selected.title && (
                  <p className="ms-detail-orig">{selected.originalTitle}</p>
                )}
                <div className="ms-detail-meta">
                  {selected.year && <span>{selected.year}</span>}
                  {selected.mediaType === 'tv' && <span>Сериал</span>}
                  {selected.rating && <span><StarIcon /> {selected.rating}</span>}
                </div>
                {selected.overview && <p className="ms-detail-overview">{selected.overview}</p>}
              </div>
            </div>

            <div className="ms-site-results">
              {sitesLoading && <p className="movie-search-message">Ищем на {sites.length} сайтах...</p>}
              {!sitesLoading && !siteResults.length && !message && <p className="movie-search-message">Поиск...</p>}
              {!sitesLoading && siteResults.length > 0 && (
                <div className="ms-sites-label">
                  Найдено на сайтах
                  <button className="ms-retry-btn" onClick={() => selected && doSiteSearch(selected.title || selected.originalTitle, sites, selected.year, selected.mediaType)} title="Повторить поиск">
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                      <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
                    </svg>
                  </button>
                </div>
              )}
              {!sitesLoading && !siteResults.length && (
                <button className="ms-retry-btn ms-retry-standalone" onClick={() => selected && doSiteSearch(selected.title || selected.originalTitle, sites, selected.year, selected.mediaType)}>
                  Повторить поиск
                </button>
              )}
              {siteResults.map((r, i) => (
                <div key={i} className="ms-site-row" onClick={() => onOpenUrl(r.title, r.url)}>
                  <span className="ms-site-source">{r.source}</span>
                  <span className="ms-site-title">{r.title}</span>
                  <div className="ms-site-actions">
                    {onBookmark && (
                      <button className="ms-bookmark-btn" title="В закладки" onClick={e => {
                        e.stopPropagation()
                        onBookmark(selected?.title || r.title, r.url, selected?.poster || '', r.source)
                      }}>
                        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
                        </svg>
                      </button>
                    )}
                    <span className="ms-site-open">Открыть →</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      ) : (
        <>
          {tmdbLoading && <p className="movie-search-message">{isSearchMode ? 'Поиск...' : 'Загрузка...'}</p>}

          {tmdbResults.length > 0 && (
            <>
              <div className="movie-results">
                {tmdbResults.map((movie, i) => <MovieCard key={movie.id} movie={movie} idx={i} baseIdx={cardBase} onSelect={handleSelectMovie} />)}
              </div>
              {!isSearchMode && page < totalPages && (
                <button className="ms-load-more" onClick={handleLoadMore} disabled={loadingMore}>
                  {loadingMore ? 'Загрузка...' : 'Загрузить ещё'}
                </button>
              )}
            </>
          )}

          {/* Direct site results (no TMDB key) */}
          {siteResults.length > 0 && (
            <div className="movie-results">
              {siteResults.map((r, i) => (
                <div key={i} className="movie-result-card" onClick={() => onOpenUrl(r.title, r.url)}>
                  <div className="movie-result-poster">
                    {r.poster
                      ? <img src={r.poster} alt={r.title} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).nextElementSibling?.removeAttribute('style') }} />
                      : null
                    }
                    <div className="movie-result-poster-placeholder" style={r.poster ? { display: 'none' } : undefined}>{r.title.charAt(0).toUpperCase()}</div>
                  </div>
                  <div className="movie-result-info">
                    <span className="movie-result-title">{r.title}</span>
                    {r.year && <span className="movie-result-year">{r.year}</span>}
                    <span className="movie-result-source">{r.source}</span>
                  </div>
                </div>
              ))}
            </div>
          )}
        </>
      )}
    </div>
  )
}

export default MovieSearch
