<!-- Плеер на itube-modern-player: тонкая обёртка над <ITubePlayer lazy>.
     lazy = SSR-заглушка (постер в первом HTML → LCP-кандидат без JS) + ленивая
     загрузка ядра по интеракции. Наше дело — source (+главы), options (иконки/локаль)
     и VAST-конфиг. Таймкоды/главы плеер рисует сам из source.chapters. -->
<template>
  <ITubePlayer
    ref="playerRef"
    v-bind="$attrs"
    :source="source"
    :options="options"
    :ad-config="loadAdConfig"
    class="player-core"
    lazy
    load-on="placeholder"
    @ended="emit('end')"
    @advancerequest="onAdvanceRequest"
    @sourcechange="onSourceChange"
    @menurequest="onMenuRequest"
  />

  <!-- Внешнее меню плеера в нашем ui-popup (только мобила — menus: 'external-mobile'). -->
  <PlayerMenu
    v-model:open="menuOpen"
    :request="menuRequest"
  />
</template>

<script setup lang="ts">
import { ITubePlayer } from 'itube-modern-player/vue';

import type { Player, PlayerOptions, VideoSource, LocaleCode, AdsOptions, PreviewMetaItem, SceneGroup, MenuRequestPayload } from 'itube-modern-player';
import { AdvertsHelper } from '../../utils/adverts-helper';
import { AdSpotType, ThumbSize  } from '../../runtime';
import type { IVideoData, IVideoCard } from '../../types';
import playIcon from '../../assets/icons/player/play.svg?raw';
import fullscreenIcon from '../../assets/icons/player/fullscreen.svg?raw';
import seekIcon from '../../assets/icons/player/seek.svg?raw';
import settingsIcon from '../../assets/icons/player/settings.svg?raw';
import channelAvatarIcon from '../../assets/icons/player/channel-avatar.svg?raw';
import actionsIcon from '../../assets/icons/player/actions.svg?raw';
import locationIcon from '../../assets/icons/player/location.svg?raw';
import modelsIcon from '../../assets/icons/player/models.svg?raw';

defineOptions({ inheritAttrs: false });

const props = defineProps<{
  data: IVideoData
  // Следующий ролик из related — нужен плееру только для ховер-превью на кнопке next.
  nextVideo?: IVideoCard
  // На странице плейлиста views в превью не показываем.
  hideViews?: boolean
}>();

// Намерение перехода при externalAdvance: кнопка next/prev, автопереход по окончании.
interface AdvanceRequest {
  direction: 'next' | 'previous' | 'select' | 'ended'
  index: number | null
  source: unknown
}

const emit = defineEmits<{
  (eventName: 'end'): void
  (eventName: 'advance', payload: AdvanceRequest): void
}>();

const { t, n, locale } = useI18n();

const playerRef = ref<InstanceType<typeof ITubePlayer> & { player: Player | null }>();

// Внешнее меню (menus: 'external-mobile'): по нажатию меню-кнопки плеер эмитит модель,
// а рисуем её мы (PlayerMenu → ui-popup). menurequest прилетает на каждое нажатие.
const menuRequest = shallowRef<MenuRequestPayload | null>(null);
const menuOpen = ref(false);

function onMenuRequest(request: MenuRequestPayload): void {
  menuRequest.value = request;
  menuOpen.value = true;
}

// Плеер эмитит advancerequest 'ended' только при включённом тумблере Autoplay, а на
// кнопку next — 'next'. Значит play после свапа нужен ТОЛЬКО когда переход инициировали
// мы (next / автопереход), а не когда source пересобрался по другой причине (сменилось
// превью следующего ролика при префетче). Ставим флаг на время перехода.
let advancing = false;

function onAdvanceRequest(payload: AdvanceRequest): void {
  if (payload.direction === 'previous') return;
  // Переключаемся всегда (грузим следующий ролик); автоплей решаем в onSourceChange.
  advancing = true;
  emit('advance', payload);
}

// player.load() сам не стартует воспроизведение. По sourcechange после нашего перехода
// доигрываем play() — но только при включённом тумблере Autoplay. Выключен → следующий
// ролик (и по next, и по окончании) только загружается и ждёт play от пользователя.
function onSourceChange(): void {
  if (!advancing) return;
  advancing = false;

  if (playerRef.value?.player?.autoAdvance) {
    playerRef.value.player.play();
  }
}

// Ключ localStorage, под которым плеер (опция persist) хранит громкость/mute и состояние
// тумблера autoplay-next.
const PLAYER_PERSIST_KEY = 'itube-player';

// Иконки плеера сырыми <svg> (?raw): itube-modern-player принимает не имя/компонент, а
// строку svg — options.icons и sceneGroups[].icon.svg. currentColor у иконок — чтобы цвет
// задавал сам плеер (solid/inverted, перекраска в меню сцен). bigPlay идёт и в SSR-заглушку
// (renderPlaceholder читает icons.bigPlay). seek одна — «назад» отражается через scaleX(-1)
// в стилях (.imp-*--seek-back svg).
const PLAYER_ICONS = {
  play: playIcon,
  bigPlay: playIcon,
  fullscreen: fullscreenIcon,
  settings: settingsIcon,
  seekForward: seekIcon,
  seekBack: seekIcon,
};

// Аватар-заглушка канала для чанка previewMeta ховер-превью (просто кружок, как в дизайне).
const CHANNEL_META_ICON = channelAvatarIcon;

// Иконки типов сцен по id группы (timelinesGroups).
const SCENE_ICONS: Record<string, string> = {
  actions: actionsIcon,
  locations: locationIcon,
  scene: modelsIcon,
};

const PLAYER_LOCALES: LocaleCode[] = ['en', 'ru', 'de', 'es', 'it', 'ja', 'ko', 'zh', 'pt', 'ar', 'hi'];
const playerLanguage = computed<LocaleCode>(() =>
    PLAYER_LOCALES.includes(locale.value as LocaleCode) ? locale.value as LocaleCode : 'en'
);

// timelinesGroups уже очищены клинером ({ id, title, scenes }) — добавляем только иконку по id.
const sceneGroups = computed<SceneGroup[] | undefined>(() => {
  const groups = props.data.timelinesGroups;
  if (!groups?.length) return undefined;

  return groups.map((group) => {
    const icon = SCENE_ICONS[group.id];
    return icon ? { ...group, icon: { svg: icon } } : group;
  });
});

// hotspots уже очищены в number[]. Оборачиваем в HeatmapData; retention — стартовый пик
// на порядок больше остального (peaks схлопнул бы кривую).
// Своего «уровня нормализации» у плеера нет (только mode/smoothing), но нормализация
// монотонна (делит на p95) — значит контраст кривой задаём сами, до передачи:
// 1) вычитаем плато (min) — провалы уходят в ноль вместо «висят на 60% высоты»;
// 2) возводим в HEATMAP_GAMMA — середина проседает, пики остаются наверху.
// На реальных hotspots (прод, видео 198149325: 96 бакетов, значения 8…20) размах кривой
// 0.25 → 0.64 высоты, т.е. ×2.5: плато опустилось с 75% до 36%, вершины остались наверху.
// ponytail: физический потолок — 0.65 высоты (в плеере захардкожен FLOOR = 0.35), выше
// уже не поднять; понадобится больше — нужен параметр floor в itube-modern-player.
const HEATMAP_GAMMA = 2;

const heatmap = computed<VideoSource['heatmap']>(() => {
  const values = props.data.hotspots;
  if (!values?.length) return undefined;

  const base = Math.min(...values);

  return { values: values.map((v) => (v - base) ** HEATMAP_GAMMA), mode: 'retention' };
});

// Постер объектом: intrinsic-размеры против CLS + srcset (Medium/Big) под LCP.
const currentSource = computed<VideoSource>(() => {
  const big = props.data.thumbUrls?.webp?.[ThumbSize.Big];
  const medium = props.data.thumbUrls?.webp?.[ThumbSize.Medium];
  const src = big || medium;

  return {
    src: props.data.videoUrl,
    type: 'application/x-mpegurl',
    title: props.data.title,
    description: props.data.description,
    poster: src
        ? {
          src,
          srcset: big && medium ? `${medium} 480w, ${big} 1280w` : undefined,
          sizes: big && medium ? '100vw' : undefined,
          width: 1280,
          height: 720,
          alt: props.data.title,
        }
        : undefined,
    thumbnails: props.data.vttUrl,
    // Главы: плеер сам рисует кнопку сцен, подписи и превью из VTT.
    // ITimestamp.duration — момент старта главы (секунды).
    chapters: props.data.actions?.map((ts) => ({ start: ts.duration, title: ts.title })),
    // timelines_groups → sceneGroups и hotspots → heatmap (маппинг выше).
    sceneGroups: sceneGroups.value,
    heatmap: heatmap.value,
    channel: props.data.channelName ? {
      name: props.data.channelName,
      avatar: props.data.channelAvatar,
    } : undefined,
  };
});

// Чанки метаданных ховер-превью: просмотры + канал (или «user uploaded» с иконкой).
function nextPreviewMeta(card: IVideoCard): PreviewMetaItem[] {
  const meta: PreviewMetaItem[] = [
    { text: card.channelName || t('user_uploaded'), icon: CHANNEL_META_ICON },
  ];
  if (!props.hideViews && card.views != null) {
    meta.push(`${n(card.views, 'compact')} ${t('views', card.views)}`);
  }
  return meta;
}

// Лёгкий source следующего ролика — только для ховер-превью. src пустой: при
// externalAdvance плеер его не грузит (реальный стрим придёт по ?view из main.vue).
const nextSource = computed<VideoSource | null>(() => {
  const card = props.nextVideo;
  if (!card) return null;

  const poster = card.thumbUrls?.webp?.[ThumbSize.Big] || card.thumbUrls?.webp?.[ThumbSize.Medium];

  return {
    src: '',
    title: card.title,
    duration: card.duration,
    poster: poster ? { src: poster, width: 1280, height: 720, alt: card.title } : undefined,
    previewMeta: nextPreviewMeta(card),
    channel: card.channelName ? { name: card.channelName, avatar: card.channelAvatar } : undefined,
  };
});

// Массив [текущий, следующий] включает режим плейлиста → плеер рисует next-превью.
// Панель плейлиста выключена (controls.playlistButton), переход — наш через ?view.
const buildSource = (): VideoSource | VideoSource[] =>
    nextSource.value ? [currentSource.value, nextSource.value] : currentSource.value;

// Пересобираем source ТОЛЬКО при смене текущего ролика (videoUrl). Иначе смена превью
// следующего (префетч) дёргала бы player.load() и перезапускала текущий ролик + ломала
// автоплей (лишние sourcechange). Один переход = один reload.
const source = shallowRef<VideoSource | VideoSource[]>(buildSource());
watch(() => props.data.videoUrl, () => {
  source.value = buildSource();
});

/**
 * Центр контрол-бара: «N просмотров · 3 месяца назад» вместо названия текущей
 * сцены (опция `controls.centerLabel`, плеер 0.11.0+). Больше этой меты на
 * странице нигде нет — под плеером её не выводим.
 *
 * Отдаём СВОЙ элемент, а не строку: кастомный центр плеер рисует один раз при
 * инициализации и больше не трогает, а ролик на странице меняется без пересоздания
 * плеера (`externalAdvance` → `player.load()`), так что строка осталась бы от
 * предыдущего ролика. Свой элемент мы просто переписываем сами.
 */
const centerLabel = shallowRef<HTMLElement | null>(null);

if (import.meta.client) {
  centerLabel.value = document.createElement('span');
}

watchEffect(() => {
  if (!centerLabel.value) return;

  const parts: string[] = [];
  if (props.data.views != null) parts.push(`${n(props.data.views, 'compact')} ${t('views', props.data.views)}`);
  if (props.data.created) parts.push(useFormatTimeAgo(props.data.created, locale.value));

  centerLabel.value.textContent = parts.join(' · ');
});

const options = computed<Omit<PlayerOptions, 'source'>>(() => ({
  playOnInit: true,
  muted: false,
  playsInline: true,
  seekStep: 15,
  playlist: {
    externalAdvance: true,
  },
  pauseScreen: false,
  language: playerLanguage.value,
  // Десктоп — нативные попапы плеера; мобила (≤767px) — эмитит menurequest, рисуем сами.
  menus: 'external-mobile',
  controls: {
    seekButtons: { label: (seconds) => String(seconds) },
    playlistButton: false,
    hideDelay: 3000,
    pip: false,
    // Кнопку списка сцен прячем, оставляем только выбор типа сцен (sceneTypes) — первой.
    scenes: false,
    // Порядок правого кластера: sceneTypes → gear (настройки) → fullscreen (последней).
    order: ['sceneTypes', 'gear', 'fullscreen'],
    // Центр бара — мета ролика вместо названия текущей сцены (см. centerLabel выше).
    centerLabel: centerLabel.value || false,
  },
  icons: PLAYER_ICONS,
  // Заголовок дропдауна выбора типа сцен (дефолт «Scene type»).
  labels: { sceneTypes: t('jump_to') },
  // Запоминаем громкость/mute в localStorage.
  persist: { key: PLAYER_PERSIST_KEY },
}));

// Async-фабрика: обёртка вызывает её параллельно загрузке ядра и конструирует плеер
// с готовым adConfig — нам не нужно синхронизировать VAST-запрос с инициализацией.
async function loadAdConfig(): Promise<AdsOptions | undefined> {
  try {
    const vastTag = await AdvertsHelper.getPrerollVastLink(AdSpotType.Primary);
    if (vastTag && vastTag.trim() !== '') {
      return { adList: [{ roll: 'preRoll', vastTag }], skipDelay: 5 };
    }
  } catch (error) {
    console.error('Failed to fetch VAST tag:', error);
  }
  return undefined;
}
</script>

<style lang="scss">
.player-core {
  position: relative;
  aspect-ratio: 16/9;
  overflow: hidden;
  width: 100%;
  border-radius: $radius-lg;

  @include to-br(sm) {
    border-radius: 0;
  }
}

// Мета ролика в центре бара: по макету белая с тенью и средней жирностью,
// у плеера по умолчанию — приглушённая и без тени.
.player-core .imp-controls__chapter {
  font-weight: 500;
  opacity: 1;
  filter: drop-shadow(0 1px 2px $player-center-label-shadow);
}
</style>
