// DramaScope — Chinese drama portal library
const { useState, useEffect, useRef, useCallback } = React;

// ── INTERNAL DATA MODEL (JSDoc) ───────────────────────────────────────────────
// Internal domain types used by DramaSourceAdapter implementations.
// Not consumed at runtime; serve as a contract for build-script output (data/*.json)
// and as documentation for adapter authors. Field names are stable across adapters.

/**
 * @typedef {Object} Drama
 * @property {number} id                  Internal stable ID (currently equals TMDB id; abstraction leak we accept until WikidataAdapter lands).
 * @property {string} slug                URL-safe идентификатор. Источник генерации в порядке приоритета: title_en → title_pinyin → translit title_ru. Из title_original (китайские иероглифы) НЕ генерируется.
 * @property {string} title_original      Оригинал (中文 / 한글 / 日本語).
 * @property {string|null} title_pinyin   Романизация (для CN/JP/KR).
 * @property {string|null} title_en       Англоязычное прокатное.
 * @property {string|null} title_ru       Русскоязычное прокатное / перевод.
 * @property {string[]} alt_titles        Альтернативные написания/варианты (включая фан-переводы, региональные релизы).
 * @property {string|null} overview_en    Из TMDB или Wikipedia EN.
 * @property {string|null} overview_ru    Авторский русский текст. По умолчанию null; заполняется вручную или build-скриптом из Wikipedia RU когда она есть.
 * @property {string|null} posterPath     Relative path; resolved via imageUrl(slug, size).
 * @property {string} firstAirDate        YYYY-MM-DD.
 * @property {number} voteAverage         0–10.
 * @property {number} voteCount
 * @property {string[]} originCountry     ISO codes: CN, HK, TW.
 * @property {Array<{id:number,name:string}>} genres
 * @property {string} status              'Returning Series' | 'Ended' | 'In Production' | 'Pilot' | 'Cancelled' | …
 * @property {number} episodeCount
 * @property {Array<{id:number,name:string}>} networks
 * @property {number} popularity
 * @property {number[]} keywordIds        References into data/keywords.json.
 * @property {Array<{personId:number,characterName:string,order:number}>} cast
 * @property {{tmdb:number,wikidata:string|null,douban:string|null,imdb:string|null}} externalIds   tmdb обязателен; остальные null если неизвестно.
 * @property {'tmdb'|'wikidata'|'manual'} _source                  Каким адаптером запись была собрана.
 * @property {string} _syncedAt                                    ISO timestamp последней синхронизации.
 */

/**
 * @typedef {Object} Person
 * @property {number} id
 * @property {string} slug
 * @property {string} name
 * @property {string} originalName
 * @property {string} biography
 * @property {string|null} profilePath
 * @property {string|null} birthday
 * @property {string|null} placeOfBirth
 * @property {number} popularity
 * @property {string[]} alsoKnownAs
 * @property {Array<{id:number,name:string,title?:string}>} knownFor
 * @property {{tmdb:number,wikidata:string|null}} externalIds
 * @property {'tmdb'|'wikidata'|'manual'} _source
 * @property {string} _syncedAt
 */

/**
 * @typedef {Object} Keyword
 * @property {number} id
 * @property {string} slug
 * @property {string} name
 * @property {number} dramaCount         Сколько дорам в текущем snapshot'е используют этот keyword.
 */

/**
 * @typedef {Object} SearchResult
 * @property {Array<Drama|Person|Keyword>} results
 * @property {number} page
 * @property {number} totalPages
 * @property {number} totalResults
 */

/**
 * @typedef {Object} HomeFeeds
 * @property {Drama[]} trending
 * @property {Drama[]} popular
 * @property {Drama[]} topRated
 * @property {Drama[]} airing
 */

/**
 * @typedef {Object} DiscoverFilters
 * @property {number[]} [genres]
 * @property {string[]} [originCountry]   ISO codes: CN, HK, TW
 * @property {number[]} [networks]
 * @property {string}   [status]
 * @property {number}   [yearFrom]
 * @property {number}   [yearTo]
 * @property {number}   [minRating]       0–10
 * @property {number}   [voteCountGte]    Minimum vote_count threshold (TMDB vote_count.gte). Перекрывает неявные 20 от minRating.
 * @property {string}   [sort]            e.g. 'popularity.desc', 'vote_average.desc'
 * @property {number[]} [castIds]
 * @property {number[]} [keywordIds]
 * @property {number}   [page]
 */

/**
 * DramaSourceAdapter — abstract data-source contract.
 *
 * Реализации:
 *   - TMDBAdapter      (tools/adapters/tmdb-adapter.js, Node-only, build-time)
 *   - JSONAdapter      (этот файл, браузер, шаг 4 миграции)
 *   - WikidataAdapter  (будущее)
 *
 * JS не предписывает наследование от @typedef — это контракт, который
 * реализации обязаны соблюдать по форме методов и возвращаемых типов.
 *
 * @typedef {Object} DramaSourceAdapter
 * @property {() => Promise<HomeFeeds>}                                                    fetchHomeFeeds
 * @property {(id:number) => Promise<Drama|null>}                                          fetchDrama
 * @property {(query:string, page?:number) => Promise<SearchResult>}                       searchDramas
 * @property {(filters:DiscoverFilters) => Promise<SearchResult>}                          discover
 * @property {(id:number) => Promise<Person|null>}                                         fetchPerson
 * @property {(id:number) => Promise<Drama[]>}                                             fetchPersonCredits
 * @property {() => Promise<Person[]>}                                                     fetchPopularActors
 * @property {(query:string) => Promise<Person[]>}                                         searchPerson
 * @property {(query:string) => Promise<Keyword[]>}                                        searchKeyword
 * @property {(posterPath:string|null, size:'w185'|'w500') => string|null}                 imageUrl
 */

// ── JSON ADAPTER ──────────────────────────────────────────────────────────────
// Browser-time реализация DramaSourceAdapter. Читает /data/*.json,
// держит каталог в памяти. Eager-loading с кэшированным Promise при первом
// async-вызове. После загрузки данных все обращения мгновенны (Map по id).
//
// Используется в production. TMDBAdapter в браузер не попадает (он Node-only
// в tools/, для build-script).
//
// JSONAdapter-specific helpers (НЕ в контракте DramaSourceAdapter):
//   - getAllDramas()           — для AI Picks (нужен весь список заголовков)
//   - getPerson(id) / getKeyword(id) — sync-getter'ы после _ensureLoaded
//   - profileImageUrl(path)    — отдельная URL-схема для фото актёров

class JSONAdapter {
  constructor({
    dataBaseUrl = './data/',
    posterBaseUrl = './assets/posters/',
    profileBaseUrl = './assets/profiles/'
  } = {}) {
    this.dataBaseUrl = dataBaseUrl;
    this.posterBaseUrl = posterBaseUrl;
    this.profileBaseUrl = profileBaseUrl;
    this._loadPromise = null;
    this._dramas = [];
    this._dramasById = new Map();
    this._people = [];
    this._peopleById = new Map();
    this._keywords = [];
    this._keywordsById = new Map();
    this._index = null;
  }

  // ─── private ─────────────────────────────────────────────────────

  async _ensureLoaded() {
    if (this._loadPromise) return this._loadPromise;
    this._loadPromise = (async () => {
      try {
        const fetchJson = async (name) => {
          const r = await fetch(this.dataBaseUrl + name);
          if (!r.ok) throw new Error(`${name}: HTTP ${r.status}`);
          return r.json();
        };
        const [dramas, people, keywords, index] = await Promise.all([
          fetchJson('dramas.json'),
          fetchJson('people.json'),
          fetchJson('keywords.json'),
          fetchJson('index.json')
        ]);

        // Перепишем posterPath/profilePath в локальный идентификатор {slug}.webp.
        // После этого imageUrl(d.posterPath, 'w500') строит ./assets/posters/w500/{slug}.webp.
        for (const d of dramas) {
          if (d.slug && d.posterPath) d.posterPath = d.slug + '.webp';
        }
        for (const p of people) {
          if (p.slug && p.profilePath) p.profilePath = p.slug + '.webp';
        }

        this._dramas = dramas;
        this._people = people;
        this._keywords = keywords;
        this._index = index;
        for (const d of dramas) this._dramasById.set(d.id, d);
        for (const p of people) this._peopleById.set(p.id, p);
        for (const k of keywords) this._keywordsById.set(k.id, k);
      } catch (e) {
        // Сбросим promise, чтобы следующий вызов мог попытаться снова.
        this._loadPromise = null;
        throw new Error(`Couldn't load site data. Please reload the page. (${e.message})`);
      }
    })();
    return this._loadPromise;
  }

  // Нормализация для substring-поиска: NFD + strip combining diacriticals + lowercase.
  // Для CJK-символов нормализация — no-op (точное совпадение).
  _normalize(s) {
    return (s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
  }

  _paginate(arr, page = 1) {
    const PAGE_SIZE = 20;
    const totalResults = arr.length;
    const totalPages = Math.max(1, Math.ceil(totalResults / PAGE_SIZE));
    const p = Math.max(1, Math.min(page, totalPages));
    return {
      results: arr.slice((p - 1) * PAGE_SIZE, p * PAGE_SIZE),
      page: p,
      totalPages,
      totalResults
    };
  }

  // ─── sync helpers (вызывать только после _ensureLoaded) ──────────

  getAllDramas() { return this._dramas; }
  getPerson(id) { return this._peopleById.get(id) || null; }
  getKeyword(id) { return this._keywordsById.get(id) || null; }

  // ─── DramaSourceAdapter contract ─────────────────────────────────

  async fetchHomeFeeds() {
    await this._ensureLoaded();
    const all = this._dramas;
    const byPop = [...all].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
    const byRating = [...all]
      .filter((d) => (d.voteCount || 0) >= 50)
      .sort((a, b) => (b.voteAverage || 0) - (a.voteAverage || 0));
    const currentYear = new Date().getUTCFullYear();
    const recent = byPop.filter((d) => {
      const y = parseInt((d.firstAirDate || '').slice(0, 4), 10);
      return y >= currentYear - 2;
    });
    const airing = byPop.filter((d) =>
      d.status === 'Returning Series' || d.status === 'In Production'
    );
    return {
      trending: recent.slice(0, 20),
      popular: byPop.slice(0, 20),
      topRated: byRating.slice(0, 20),
      airing: airing.slice(0, 20)
    };
  }

  async fetchDrama(id) {
    await this._ensureLoaded();
    return this._dramasById.get(id) || null;
  }

  async searchDramas(query, page = 1) {
    await this._ensureLoaded();
    const q = this._normalize(query);
    if (!q) return { results: [], page: 1, totalPages: 1, totalResults: 0 };
    const qRaw = (query || '').toLowerCase();  // для title_original (CJK без нормализации)
    const matches = this._dramas.filter((d) => {
      if (d.title_en && this._normalize(d.title_en).includes(q)) return true;
      if (d.title_ru && this._normalize(d.title_ru).includes(q)) return true;
      if (d.title_pinyin && this._normalize(d.title_pinyin).includes(q)) return true;
      if (d.title_original && d.title_original.toLowerCase().includes(qRaw)) return true;
      if (d.alt_titles && d.alt_titles.some((t) => this._normalize(t).includes(q))) return true;
      return false;
    });
    matches.sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
    return this._paginate(matches, page);
  }

  async discover(filters = {}) {
    await this._ensureLoaded();
    let arr = [...this._dramas];

    if (filters.genres && filters.genres.length) {
      const want = new Set(filters.genres);
      arr = arr.filter((d) => (d.genres || []).some((g) => want.has(g.id)));
    }
    if (filters.originCountry && filters.originCountry.length) {
      const want = new Set(filters.originCountry);
      arr = arr.filter((d) => (d.originCountry || []).some((c) => want.has(c)));
    }
    if (filters.networks && filters.networks.length) {
      const want = new Set(filters.networks);
      arr = arr.filter((d) => (d.networks || []).some((n) => want.has(n.id)));
    }
    if (filters.status !== undefined && filters.status !== '' && filters.status !== null) {
      const STATUS_MAP = {
        '0': 'Returning Series', '1': 'Planned', '2': 'In Production',
        '3': 'Ended', '4': 'Cancelled', '5': 'Pilot'
      };
      const target = STATUS_MAP[filters.status] || filters.status;
      arr = arr.filter((d) => d.status === target);
    }
    if (filters.yearFrom) {
      arr = arr.filter((d) => {
        const y = parseInt((d.firstAirDate || '').slice(0, 4), 10);
        return y && y >= filters.yearFrom;
      });
    }
    if (filters.yearTo) {
      arr = arr.filter((d) => {
        const y = parseInt((d.firstAirDate || '').slice(0, 4), 10);
        return y && y <= filters.yearTo;
      });
    }
    if (filters.minRating && filters.minRating > 0) {
      arr = arr.filter((d) => (d.voteAverage || 0) >= filters.minRating);
    }
    if (filters.voteCountGte) {
      arr = arr.filter((d) => (d.voteCount || 0) >= filters.voteCountGte);
    }
    if (filters.castIds && filters.castIds.length) {
      const want = new Set(filters.castIds);
      arr = arr.filter((d) => (d.cast || []).some((c) => want.has(c.personId)));
    }
    if (filters.keywordIds && filters.keywordIds.length) {
      const want = new Set(filters.keywordIds);
      arr = arr.filter((d) => (d.keywordIds || []).some((k) => want.has(k)));
    }

    if (filters.sort) {
      const [rawField, dir] = filters.sort.split('.');
      const FIELD_MAP = {
        popularity: 'popularity',
        vote_average: 'voteAverage',
        vote_count: 'voteCount',
        first_air_date: 'firstAirDate'
      };
      const field = FIELD_MAP[rawField] || rawField;
      const mult = dir === 'asc' ? 1 : -1;
      arr.sort((a, b) => {
        const va = a[field], vb = b[field];
        if (typeof va === 'string' || typeof vb === 'string') {
          return mult * String(va || '').localeCompare(String(vb || ''));
        }
        return mult * ((va || 0) - (vb || 0));
      });
    }

    return this._paginate(arr, filters.page || 1);
  }

  async fetchPerson(id) {
    await this._ensureLoaded();
    return this._peopleById.get(id) || null;
  }

  async fetchPersonCredits(id) {
    await this._ensureLoaded();
    return this._dramas.filter((d) =>
      (d.cast || []).some((c) => c.personId === id)
    );
  }

  async fetchPopularActors() {
    await this._ensureLoaded();
    return [...this._people].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
  }

  async searchPerson(query) {
    await this._ensureLoaded();
    const q = this._normalize(query);
    if (!q) return [];
    const qRaw = (query || '').toLowerCase();
    return this._people.filter((p) => {
      if (p.name && this._normalize(p.name).includes(q)) return true;
      if (p.originalName && p.originalName.toLowerCase().includes(qRaw)) return true;
      if (p.alsoKnownAs && p.alsoKnownAs.some((n) => this._normalize(n).includes(q))) return true;
      return false;
    }).sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
  }

  async searchKeyword(query) {
    await this._ensureLoaded();
    const q = this._normalize(query);
    if (!q) return [];
    return this._keywords.filter((k) =>
      k.name && this._normalize(k.name).includes(q)
    );
  }

  imageUrl(path, size) {
    if (!path) return null;
    return `${this.posterBaseUrl}${size}/${path}`;
  }

  // JSONAdapter-specific: профили лежат плоско в assets/profiles/{slug}.webp,
  // без size-папок. Используется компонентами для фото актёров.
  profileImageUrl(path) {
    if (!path) return null;
    return `${this.profileBaseUrl}${path}`;
  }
}

// ── ADAPTER HOOK ──────────────────────────────────────────────────────────────
// React-хук поверх DramaSourceAdapter. fetcher принимает window.adapter.
// Возвращает {data, loading, error}. Гонка с unmount защищена флагом cancelled.
function useAdapter(fetcher, deps = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  useEffect(() => {
    setLoading(true); setError(null);
    let cancelled = false;
    fetcher(window.adapter)
      .then((d) => { if (!cancelled) { setData(d); setLoading(false); } })
      .catch((e) => { if (!cancelled) { setError(e); setLoading(false); } });
    return () => { cancelled = true; };
  }, deps);
  return { data, loading, error };
}

// ── SMALL UI ──────────────────────────────────────────────────────────────────
function DSBadge({ label, variant = 'blue' }) {
  const map = {
    blue: { bg: 'rgba(74,158,255,0.15)', color: '#7bc3ff', border: 'rgba(74,158,255,0.25)' },
    green: { bg: 'rgba(61,214,140,0.12)', color: '#3dd68c', border: 'rgba(61,214,140,0.25)' },
    red: { bg: 'rgba(255,107,107,0.12)', color: '#ff6b6b', border: 'rgba(255,107,107,0.25)' },
    gold: { bg: 'rgba(245,197,24,0.12)', color: '#f5c518', border: 'rgba(245,197,24,0.25)' },
    gray: { bg: 'rgba(255,255,255,0.07)', color: '#8ba8d4', border: 'rgba(255,255,255,0.1)' },
    purple: { bg: 'rgba(192,132,252,0.12)', color: '#c084fc', border: 'rgba(192,132,252,0.25)' }
  };
  const s = map[variant] || map.blue;
  return (
    <span style={{ ...{
        display: 'inline-block', padding: '2px 9px', borderRadius: 4,
        background: s.bg, color: s.color, border: `1px solid ${s.border}`,
        fontSize: 11, fontWeight: 600, letterSpacing: '0.3px', whiteSpace: 'nowrap'
      }, color: "rgb(255, 224, 224)" }}>{label}</span>);

}

function DSStars({ vote }) {
  return (
    <span style={{ color: '#f5c518', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
      ★ <span style={{ color: '#e0e0e0', fontSize: 12 }}>{Math.round(vote * 10) / 10}</span>
    </span>);

}

function DSSpinner() {
  return (
    <div style={{ display: 'flex', justifyContent: 'center', padding: '40px 0' }}>
      <div style={{ width: 30, height: 30, border: '3px solid rgba(74,158,255,0.15)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
    </div>);

}

const POSTER_GRADIENTS = [
['#1a0a1a', '#3a1040'], ['#2a1010', '#4a1820'], ['#0e1f44', '#1a3a6e'],
['#1a1040', '#3a205c'], ['#14102a', '#2a2050'], ['#0a2030', '#1a4060'],
['#0a2010', '#1a4030'], ['#1a1a10', '#3a3020'], ['#0e2a1a', '#1a5030']];


function DSPlaceholderPoster({ seed = 1, width = '100%', height = '100%', style = {} }) {
  const [c1, c2] = POSTER_GRADIENTS[(seed - 1) % POSTER_GRADIENTS.length];
  const uid = `pp${seed}`;
  return (
    <svg width={width} height={height} viewBox="0 0 120 170" preserveAspectRatio="xMidYMid slice" style={style}>
      <defs>
        <linearGradient id={uid} x1="0" y1="0" x2="1" y2="1">
          <stop offset="0%" stopColor={c1} /><stop offset="100%" stopColor={c2} />
        </linearGradient>
      </defs>
      <rect width="120" height="170" fill={`url(#${uid})`} />
      {Array.from({ length: 10 }).map((_, i) =>
      <line key={i} x1="0" y1={i * 18} x2="120" y2={i * 18} stroke="rgba(255,255,255,0.03)" strokeWidth="1" />
      )}
      <rect x="28" y="42" width="64" height="86" rx="3" fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth="1" />
      <text x="60" y="88" textAnchor="middle" fill="rgba(255,255,255,0.18)" fontSize="10" fontFamily="monospace">海报</text>
    </svg>);

}

function DSPlaceholderBackdrop({ seed = 1, style = {} }) {
  const clrs = [['#12082a', '#221044'], ['#060e28', '#0d1e44'], ['#041822', '#0a2c3a']];
  const [c1, c2] = clrs[(seed - 1) % clrs.length];
  const uid = `bd${seed}`;
  return (
    <svg width="100%" height="100%" viewBox="0 0 1280 720" preserveAspectRatio="xMidYMid slice" style={style}>
      <defs>
        <linearGradient id={uid} x1="0" y1="0" x2="1" y2="1">
          <stop offset="0%" stopColor={c1} /><stop offset="100%" stopColor={c2} />
        </linearGradient>
      </defs>
      <rect width="1280" height="720" fill={`url(#${uid})`} />
      {Array.from({ length: 18 }).map((_, i) =>
      <line key={i} x1="0" y1={i * 42} x2="1280" y2={i * 42} stroke="rgba(255,255,255,0.025)" strokeWidth="1" />
      )}
    </svg>);

}

// ── DRAMA CARD ────────────────────────────────────────────────────────────────
const FLAG = { CN: '🇨🇳', HK: '🇭🇰', TW: '🇹🇼' };

function DramaCard({ item, onClick, size = 'md', onToggleWatch, inWatchlist }) {
  const [hov, setHov] = useState(false);
  const [imgErr, setImgErr] = useState(false);
  const posterUrl = !imgErr && window.adapter.imageUrl(item.posterPath, size === 'lg' ? 'w500' : 'w185');
  const widths = { sm: 128, md: 165, lg: 195 };
  const w = widths[size] || 165;
  const h = Math.round(w * 1.5);
  const year = (item.firstAirDate || '').slice(0, 4);
  const flag = (item.originCountry || []).map((c) => FLAG[c]).find(Boolean) || '🇨🇳';
  const displayTitle = item.title_en || item.title_pinyin || item.title_original || '';

  return (
    <div onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{
      flex: `0 0 ${w}px`, width: w, cursor: 'pointer', position: 'relative',
      transform: hov ? 'translateY(-4px)' : 'none', transition: 'transform 0.22s',
      zIndex: hov ? 10 : 1
    }}>
      <div style={{ width: w, height: h, borderRadius: 8, overflow: 'hidden', background: '#0a0e1c',
        boxShadow: hov ? '0 8px 28px rgba(0,0,0,0.55)' : 'none', transition: 'box-shadow 0.22s' }}>
        {posterUrl ?
        <img src={posterUrl} alt={displayTitle} onError={() => setImgErr(true)} className="ds-poster" style={{ width: '100%', height: '100%', display: 'block' }} /> :
        <DSPlaceholderPoster seed={(item.id || 1) % 9 + 1} width="100%" height="100%" />
        }
        <div style={{ position: 'absolute', top: 6, left: 6, display: 'flex', gap: 4, alignItems: 'center' }}>
          {flag && <span style={{ fontSize: 13 }}>{flag}</span>}
        </div>
        {item.voteAverage > 0 &&
        <div style={{ position: 'absolute', top: 6, right: 6, background: 'rgba(0,0,0,0.78)', borderRadius: 4, padding: '2px 6px', fontSize: 11, fontWeight: 700, color: '#f5c518', backdropFilter: 'blur(4px)' }}>
            ★ {Math.round(item.voteAverage * 10) / 10}
          </div>
        }
        <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.3) 50%, transparent 100%)', opacity: hov ? 1 : 0, transition: 'opacity 0.2s' }} />
        {hov &&
        <div style={{ position: 'absolute', bottom: 8, left: 8, right: 8 }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: '#fff', lineHeight: 1.3, marginBottom: 6 }}>{displayTitle}</div>
            <div style={{ display: 'flex', gap: 6 }}>
              <DSBadge label="View Details" variant="blue" />
              {onToggleWatch &&
            <button onClick={(e) => {e.stopPropagation();onToggleWatch(item);}} style={{
              padding: '2px 8px', borderRadius: 4, fontSize: 11, fontWeight: 600,
              background: inWatchlist ? 'rgba(61,214,140,0.2)' : 'rgba(74,158,255,0.2)',
              color: inWatchlist ? '#3dd68c' : '#7bc3ff',
              border: `1px solid ${inWatchlist ? 'rgba(61,214,140,0.3)' : 'rgba(74,158,255,0.3)'}`
            }}>{inWatchlist ? '✓ Saved' : '+ Watchlist'}</button>
            }
            </div>
          </div>
        }
      </div>
      <div style={{ padding: '8px 2px 4px' }}>
        <div style={{ fontSize: 12, fontWeight: 500, color: hov ? '#fff' : '#c8d8f0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'color 0.15s' }}>
          {displayTitle}
        </div>
        {year && <div style={{ fontSize: 11, color: '#4d6a9a', marginTop: 1 }}>{year}</div>}
      </div>
    </div>);

}

// ── CAROUSEL ──────────────────────────────────────────────────────────────────
function DSCarousel({ title, items, onSelect, icon = '', badge, viewAll, onToggleWatch, watchlist }) {
  const scrollRef = useRef(null);
  const [canLeft, setCanLeft] = useState(false);
  const [canRight, setCanRight] = useState(true);

  const check = () => {
    const el = scrollRef.current;if (!el) return;
    setCanLeft(el.scrollLeft > 8);
    setCanRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 8);
  };
  const scroll = (dir) => {
    scrollRef.current?.scrollBy({ left: dir * 560, behavior: 'smooth' });
    setTimeout(check, 350);
  };

  if (!items || !items.length) return null;

  return (
    <div style={{ marginBottom: 44 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, padding: '0 24px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          {icon && <span style={{ fontSize: 18 }}>{icon}</span>}
          <h2 style={{ fontSize: 17, fontWeight: 700, color: "rgb(34, 78, 159)" }}>{title}</h2>
          {badge && <DSBadge label={badge} variant="gold" />}
        </div>
        {viewAll && <button onClick={viewAll} style={{ fontSize: 12, color: 'var(--accent)' }}>View all →</button>}
      </div>
      <div style={{ position: 'relative' }}>
        {canLeft &&
        <button onClick={() => scroll(-1)} style={{ position: 'absolute', left: 4, top: '40%', transform: 'translateY(-50%)', zIndex: 20, width: 34, height: 34, borderRadius: '50%', background: 'rgba(8,10,22,0.92)', border: '1px solid rgba(74,158,255,0.3)', color: '#fff', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(8px)' }}>‹</button>
        }
        {canRight &&
        <button onClick={() => scroll(1)} style={{ position: 'absolute', right: 4, top: '40%', transform: 'translateY(-50%)', zIndex: 20, width: 34, height: 34, borderRadius: '50%', background: 'rgba(8,10,22,0.92)', border: '1px solid rgba(74,158,255,0.3)', color: '#fff', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(8px)' }}>›</button>
        }
        <div ref={scrollRef} onScroll={check} style={{ display: 'flex', gap: 14, overflowX: 'auto', overflowY: 'visible', padding: '4px 24px 12px', scrollbarWidth: 'none', msOverflowStyle: 'none', fontSize: "18px" }}>
          {items.map((item) =>
          <DramaCard
            key={item.id}
            item={item}
            onClick={() => onSelect(item)}
            onToggleWatch={onToggleWatch}
            inWatchlist={watchlist?.some((w) => w.id === item.id)} />

          )}
        </div>
      </div>
    </div>);

}

// ── TIME AGO ──────────────────────────────────────────────────────────────────
function timeAgo(dateStr) {
  const d = new Date(dateStr);
  if (isNaN(d)) return dateStr;
  const h = Math.floor((Date.now() - d.getTime()) / 3600000);
  if (h < 1) return 'just now';
  if (h < 24) return `${h} hours ago`;
  const days = Math.floor(h / 24);
  if (days === 1) return 'yesterday';
  if (days < 7) return `${days} days ago`;
  return dateStr;
}

// ── NEWS CARD ─────────────────────────────────────────────────────────────────
function NewsCard({ news, variant = 'hero', onClick }) {
  if (!news) return null;

  const HotBadge = () => news.hot && (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 3,
      padding: '2px 7px', borderRadius: 999,
      background: 'linear-gradient(135deg,#ff6b35,#ff3d3d)',
      color: '#fff', fontSize: variant === 'hero' ? 9.5 : 8.5, fontWeight: 800,
      letterSpacing: 0.8, textTransform: 'uppercase',
      lineHeight: 1, boxShadow: '0 1px 4px rgba(255,61,61,0.4)'
    }}>🔥 HOT</span>
  );

  if (variant === 'hero') {
    return (
      <div onClick={onClick} style={{
        position: 'relative', width: 440, height: 140,
        margin: '14px 0 20px 0', borderRadius: 12, overflow: 'hidden',
        backgroundImage: news.image ? `url(${news.image})` : 'linear-gradient(135deg,#2c3e50 0%, #4a6178 100%)',
        backgroundSize: 'cover', backgroundPosition: 'center',
        boxShadow: '0 4px 16px rgba(0,0,0,0.18)',
        cursor: onClick ? 'pointer' : 'default'
      }}>
        <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(90deg, rgba(15,25,35,0.92) 0%, rgba(15,25,35,0.7) 45%, rgba(15,25,35,0.25) 85%, transparent 100%)' }} />
        <div style={{ position: 'absolute', left: 18, right: 18, top: 18, bottom: 14, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
          <div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
              <div style={{ color: '#4a9eff', fontSize: 11, fontWeight: 700, letterSpacing: 1.2 }}>NEWS</div>
              <HotBadge />
            </div>
            <div style={{ color: '#fff', fontSize: 15, fontWeight: 700, lineHeight: 1.25, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{news.title}</div>
          </div>
          <div style={{ color: 'rgba(220,225,235,0.78)', fontSize: 11, display: 'flex', alignItems: 'center', gap: 14 }}>
            <span>{timeAgo(news.date)}</span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
              <span style={{ color: '#ff7a8a' }}>♥</span><span>{news.likes ?? 31}</span>
            </span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
              💬 <span>{news.comments ?? 23}</span>
            </span>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div onClick={onClick} style={{
      padding: '10px 0', borderBottom: '1px solid rgba(0,0,0,0.06)',
      cursor: onClick ? 'pointer' : 'default'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
        <span style={{ color: '#6b6b6b', fontSize: 9.5, fontWeight: 700, letterSpacing: 0.6, textTransform: 'uppercase' }}>{news.category}</span>
        <HotBadge />
      </div>
      <div style={{ color: '#1f1f1f', fontSize: 13, fontWeight: 600, lineHeight: 1.3, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{news.title}</div>
      <div style={{ color: '#8a8a8a', fontSize: 10.5, marginTop: 4 }}>{timeAgo(news.date)}</div>
    </div>
  );
}

// ── FEATURED HERO ─────────────────────────────────────────────────────────────
function DSFeatured({ item, onSelect, onExplore, onActors, onAI }) {
  const [imgErr, setImgErr] = useState(false);
  const backdropUrl = "./assets/cherry-blossom-banner.png";
  const year = (item.firstAirDate || '').slice(0, 4);
  const synopsis = item.overview_en || '';
  const short = synopsis.length > 180 ? synopsis.slice(0, 180) + '…' : synopsis;
  const displayTitle = item.title_en || item.title_pinyin || item.title_original || '';
  const miniPoster = window.adapter.imageUrl(item.posterPath, 'w185');

  return (
    <div style={{ position: 'relative', width: '100%', height: 'clamp(440px,58vw,680px)', overflow: 'hidden' }}>
      {backdropUrl ?
      <img src={backdropUrl} alt="" onError={() => setImgErr(true)} className="ds-backdrop" style={{ width: '100%', height: '100%', display: 'block' }} /> :
      <DSPlaceholderBackdrop seed={(item.id || 1) % 3 + 1} style={{ position: 'absolute', inset: 0 }} />
      }
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to right, rgba(255,247,241,0.45) 0%, rgba(255,247,241,0.18) 45%, rgba(255,247,241,0.05) 80%, transparent 100%)' }} />
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(251,239,230,0.85) 0%, rgba(251,239,230,0.15) 30%, transparent 60%)' }} />

      <div style={{ position: 'absolute', bottom: 0, left: 0, padding: '0 48px 52px', maxWidth: 680 }}>
        <h1 style={{ fontWeight: 800, lineHeight: 1.05, marginBottom: 8, letterSpacing: '-0.5px', fontSize: "32px", fontFamily: "Helvetica" }}>
          Discover Chinese<br />Dramas & Actors
        </h1>
        <NewsCard news={(window.NEWS_DATA || [])[0]} variant="hero" />
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
          <button onClick={onExplore} className="ds-btn-primary" style={{ fontSize: 13.5 }}>Explore C-Dramas</button>
          <button onClick={onActors} className="ds-btn-secondary" style={{ fontSize: 13.5 }}>Browse Chinese Actors</button>
          <button onClick={onAI} className="ds-ai-recommend" style={{
            display: 'flex', alignItems: 'center', gap: 6, fontSize: 16
          }}>✦ Get AI Recommendations</button>
        </div>

        {/* Featured drama mini card */}
        <div style={{ marginTop: 32, display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', borderRadius: 14, border: 'none', maxWidth: 480, cursor: 'pointer', background: '#FFF7F1', color: '#5A4438', boxShadow: '0 6px 18px rgba(90,68,56,0.10)' }}
        onClick={() => onSelect(item)}>
          <div style={{ width: 46, height: 46, borderRadius: 6, overflow: 'hidden', flexShrink: 0 }}>
            {miniPoster ?
            <img src={miniPoster} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> :
            <DSPlaceholderPoster seed={(item.id || 1) % 9 + 1} width="46" height="46" />
            }
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{display:'flex', alignItems:'center', gap:8, marginBottom:2}}>
              <span style={{
                display:'inline-flex',
                alignItems:'center',
                gap:6,
                padding:'2px 8px',
                borderRadius:999,
                background:'rgba(180,130,120,0.20)',
                border:'1px solid rgba(180,130,120,0.40)',
                color:'#5a4438',
                fontSize:9,
                fontWeight:600,
                letterSpacing:'0.3px',
                textTransform:'uppercase',
                lineHeight:1
              }}>🇨🇳 Drama of the Week</span>
              <span style={{color:'#6b6b6b', fontSize:11, fontWeight:400, lineHeight:1}}>
                {year || '—'}
              </span>
              <span style={{display:'flex', alignItems:'center', gap:3, lineHeight:1}}>
                <span style={{color:'#d4a017', fontSize:12}}>★</span>
                <span style={{color:'#3a3a3a', fontSize:13, fontWeight:700}}>
                  {item.voteAverage ? item.voteAverage.toFixed(1) : '—'}
                </span>
              </span>
            </div>
            <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayTitle}</div>
          </div>
          <span style={{ color: 'var(--accent)', fontSize: 12 }}>→</span>
        </div>
      </div>
    </div>);

}

// ── DETAIL PAGE ───────────────────────────────────────────────────────────────
function DSDetailPage({ apiKey, itemId, onBack, onSelectActor, onToggleWatch, watchlist }) {
  // apiKey оставлен в сигнатуре для backwards-compat — App ещё его прокидывает.
  // В 4B-2 уберём и из сигнатуры, и из вызывающего кода.
  const { data: show, loading } = useAdapter((a) => a.fetchDrama(itemId), [itemId]);

  // Similar — через discover по жанрам когда show загружен
  const showGenreIds = (show?.genres || []).map((g) => g.id);
  const { data: similar } = useAdapter(
    (a) => showGenreIds.length ?
      a.discover({ genres: showGenreIds, page: 1 }) :
      Promise.resolve({ results: [] }),
    [show?.id, showGenreIds.join(',')]
  );

  if (loading) return <div style={{ paddingTop: 100 }}><DSSpinner /></div>;
  if (!show) return null;

  const heroBackdropUrl = "./assets/cherry-blossom-banner.png";
  const cast = (show.cast || []).slice(0, 14);
  const simItems = (similar?.results || []).filter((s) => s.id !== show.id && s.posterPath).slice(0, 8);
  const flag = (show.originCountry || []).map((c) => FLAG[c]).find(Boolean) || '🇨🇳';
  const inWatch = watchlist?.some((w) => w.id === show.id);

  const displayTitle = show.title_en || show.title_pinyin || show.title_original || '';
  const displayOverview = show.overview_en || 'No synopsis available.';
  const posterUrl = window.adapter.imageUrl(show.posterPath, 'w500');
  const subscript = [show.title_original, show.title_pinyin].filter(Boolean).join(' · ');

  return (
    <div>
      <div style={{ padding: '14px 24px 0', maxWidth: 1280, margin: '0 auto' }}>
        <button onClick={onBack} style={{ color: 'var(--accent)', fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 }}>← Back</button>
      </div>
      <div style={{ position: 'relative', height: 'clamp(240px,30vw,420px)', overflow: 'hidden' }}>
        <img src={heroBackdropUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', opacity: 0.7, display: 'block' }} />
        <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(251,239,230,0.85) 0%, rgba(251,239,230,0.30) 40%, rgba(251,239,230,0.20) 100%)' }} />
      </div>
      <div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 24px' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '175px 1fr', gap: 28, marginTop: -72, position: 'relative', zIndex: 2, alignItems: 'start' }}>
          <div style={{ borderRadius: 10, overflow: 'hidden', boxShadow: '0 8px 30px rgba(90,68,56,0.20)', border: '1px solid var(--border)', aspectRatio: '2 / 3', height: 'auto' }}>
            {posterUrl ?
            <img src={posterUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> :
            <DSPlaceholderPoster seed={(show.id || 1) % 9 + 1} width="100%" height="100%" />
            }
          </div>
          <div style={{ paddingTop: 76 }}>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 20 }}>{flag}</span>
              {(show.genres || []).slice(0, 3).map((g) => <DSBadge key={g.id} label={g.name} />)}
            </div>
            <h1 style={{ fontSize: 'clamp(20px,2.5vw,34px)', fontWeight: 800, lineHeight: 1.2, marginBottom: 4 }}>{displayTitle}</h1>
            {subscript && <div style={{ fontSize: 14, color: 'var(--text3)', marginBottom: 12 }}>{subscript}</div>}
            <div className="ds-drama-stats" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginBottom: 14 }}>
              {show.voteAverage > 0 && <>
                <span className="ds-rating-star">★</span>
                <span className="ds-rating-num">{show.voteAverage.toFixed(1)}</span>
              </>}
              {show.firstAirDate && <span className="ds-year">{show.firstAirDate.slice(0, 4)}</span>}
              {show.episodeCount > 0 && <span className="ds-episodes">{show.episodeCount} ep.</span>}
              {show.status && (() => {
                const label = show.status === 'Ended' ? 'Completed' : show.status === 'Returning Series' ? 'Ongoing' : show.status;
                const cls = label === 'Completed' ? 'is-completed' : label === 'Ongoing' ? 'is-ongoing' : 'is-upcoming';
                return <span className={`ds-status-badge ${cls}`}>{label}</span>;
              })()}
            </div>
            {show.networks?.length > 0 &&
            <div className="ds-drama-meta" style={{ marginBottom: 14 }}>
                <span className="ds-label">Network:</span>{' '}
                {show.networks.map((n) =>
                  <span key={n.id} className="ds-network-name">{n.name}</span>
                ).reduce((prev, curr) => [prev, ' ', curr])}
              </div>
            }
            <p style={{ fontSize: 14, lineHeight: 1.85, color: 'var(--text2)', maxWidth: 580, marginBottom: 18 }}>{displayOverview}</p>

            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
              {window.LibraryStatusButton && <window.LibraryStatusButton item={show} />}
              {window.ShareMenu && <window.ShareMenu title={displayTitle} />}
            </div>
          </div>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(140px,1fr))', gap: 12, marginTop: 28, marginBottom: 28 }}>
          {[
          show.firstAirDate && ['First Air', show.firstAirDate.slice(0, 4)],
          show.episodeCount > 0 && ['Episodes', show.episodeCount],
          show.voteAverage > 0 && ['Rating', show.voteAverage.toFixed(1) + ' / 10'],
          show.voteCount > 0 && ['Votes', show.voteCount.toLocaleString()],
          (show.originCountry || []).length > 0 && ['Origin', (show.originCountry || []).map((c) => FLAG[c] || c).join(' ')]].
          filter(Boolean).map(([k, v]) =>
          <div key={k} style={{ background: 'var(--bg2)', borderRadius: 8, padding: '12px 14px', border: '1px solid rgba(74,158,255,0.08)' }}>
              <div style={{ fontSize: 10, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 4 }}>{k}</div>
              <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text)' }}>{v}</div>
            </div>
          )}
        </div>

        {cast.length > 0 &&
        <div style={{ marginBottom: 28 }}>
            <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>Cast</h2>
            <div style={{ display: 'flex', gap: 14, overflowX: 'auto', paddingBottom: 8, scrollbarWidth: 'none' }}>
              {cast.map((c) => {
                const person = window.adapter.getPerson(c.personId);
                const photoUrl = person?.profilePath ? window.adapter.profileImageUrl(person.profilePath) : null;
                const initial = ((c.characterName || person?.name || '?').trim().charAt(0) || '?').toUpperCase();
                return (
                  <div key={c.personId} onClick={() => person && onSelectActor && onSelectActor(c.personId)} style={{ flex: '0 0 80px', textAlign: 'center', cursor: person ? 'pointer' : 'default' }}>
                    <div style={{ width: 80, height: 80, borderRadius: '50%', overflow: 'hidden', border: '2px solid rgba(74,158,255,0.15)', marginBottom: 6, background: 'var(--bg3)' }}>
                      {photoUrl ?
                        <img src={photoUrl} alt={person?.name || ''} className="ds-avatar" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> :
                        <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24, fontWeight: 700, color: 'var(--text2)', background: 'var(--bg4)' }}>{initial}</div>
                      }
                    </div>
                    <div style={{ fontSize: 11, fontWeight: 500, lineHeight: 1.3 }}>{person?.name || '—'}</div>
                    <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 1 }}>{c.characterName}</div>
                  </div>
                );
              })}
            </div>
          </div>
        }

        {/* Ratings & reactions (gated for guests inside the components) */}
        {window.DramaRatingPanel && <window.DramaRatingPanel dramaId={show.id} />}
        {window.DramaReactionPanel && <window.DramaReactionPanel dramaId={show.id} />}
        {window.RecommendBlock && <window.RecommendBlock dramaId={show.id} />}
        {window.DramaAIButtons && <window.DramaAIButtons />}
        {window.ReviewsSection && <window.ReviewsSection dramaId={show.id} />}

        {simItems.length > 0 &&
        <div style={{ marginTop: 32, marginBottom: 48 }}>
            <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>Similar C-Dramas</h2>
            <div style={{ display: 'flex', gap: 12, overflowX: 'auto', paddingBottom: 8, scrollbarWidth: 'none' }}>
              {simItems.map((s) => <DramaCard key={s.id} item={s} onClick={() => window.__dsSelectItem(s.id)} />)}
            </div>
          </div>
        }
      </div>
    </div>);

}

// Export to window
Object.assign(window, {
  FLAG, useAdapter,
  DSBadge, DSStars, DSSpinner, DSPlaceholderPoster, DSPlaceholderBackdrop,
  DramaCard, DSCarousel, DSFeatured, DSDetailPage
});