<template>
  <GuidePageFrame
    :active-tab="activeTopTab"
    variant="static"
    :show-top-tabs="false"
    @back="handleBack"
  >
    <view class="explain-detail-page">
      <view class="immersive-hero" :class="{ 'has-real-image': heroImage }">
        <image
          v-if="heroImage"
          class="hero-real-image"
          :src="heroImage"
          mode="aspectFill"
        />
        <view class="hero-image-shade"></view>
        <button
          v-if="!shouldUseHostNavigation"
          class="hero-back"
          aria-label="返回讲解详情"
          @tap.stop="handleBack"
        >
          <text class="hero-back-icon">‹</text>
        </button>
        <view class="hero-copy">
          <text class="detail-title">{{ displayTitle }}</text>
          <text class="detail-meta">{{ heroSubtitle }}</text>
          <view class="detail-chip-row">
            <view class="detail-chip shared">
              <text class="detail-chip-text">标准解说</text>
            </view>
          </view>
        </view>
      </view>
      <view v-if="contentLanguageOptions.length" class="detail-language-bar">
        <view class="language-switch" role="tablist" aria-label="讲解语言">
          <button
            v-for="option in contentLanguageOptions"
            :key="option.value"
            class="language-option"
            :class="{
              active: selectedAudioLanguage === option.value,
              disabled: languageSwitchLoading
            }"
            :disabled="languageSwitchLoading"
            :aria-selected="selectedAudioLanguage === option.value"
            :aria-label="`切换至${option.label}`"
            role="tab"
            @tap="handleLanguageChange(option.value)"
          >
            <text class="language-option-text">{{ option.label }}</text>
          </button>
        </view>
      </view>
      <scroll-view
        class="content"
        scroll-y
        :show-scrollbar="false"
      >
        <view class="detail-content">
          <view class="content-section" :class="{ 'is-english': selectedAudioLanguage === 'en-US' }">
            <template v-if="currentDetailParagraphs.length">
              <text
                v-for="(paragraph, paragraphIndex) in currentDetailParagraphs"
                :key="`${selectedAudioLanguage}-${paragraphIndex}`"
                class="section-text"
              >
                {{ paragraph }}
              </text>
            </template>
            <text v-if="currentDetailTextLoading" class="section-hint">{{ detailLoadingMessage }}</text>
          </view>
        </view>
      </scroll-view>

      <view class="detail-audio-dock" :class="`is-${audioDockState}`">
        <view class="detail-audio-dock-main">
          <button
            class="detail-audio-play"
            :class="{ disabled: audioDockState === 'unavailable' }"
            :disabled="audioDockState === 'unavailable'"
            :aria-label="detailAudioActionLabel"
            @tap="handlePlayAudio"
          >
            <view v-if="audioDockState === 'loading'" class="audio-loading-spinner" aria-label="音频加载中"></view>
            <svg v-else-if="detailAudioPlaying" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor"/>
              <rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor"/>
            </svg>
            <svg v-else width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="M8 5V19L19 12L8 5Z" fill="currentColor"/>
            </svg>
          </button>
          <view class="detail-audio-main">
            <text class="detail-audio-title">{{ audioDockTitle }}</text>
            <text class="detail-audio-subtitle">{{ audioDockSubtitle }}</text>
          </view>
          <button
            v-if="audioDockState === 'failed'"
            class="detail-audio-retry"
            aria-label="重试播放音频"
            @tap="handleRetryAudio"
          >重试</button>
        </view>
        <view v-if="audioDockState === 'loading'" class="detail-audio-progress is-indeterminate" aria-label="音频加载中"></view>
        <view
          v-else
          class="detail-audio-progress-hit"
          :class="{ disabled: audioDockState !== 'playable' }"
          @tap="handleSeekAudio"
        >
          <view class="detail-audio-progress" aria-hidden="true">
            <view class="detail-audio-progress-fill" :style="{ width: `${detailAudioProgressPercent}%` }"></view>
            <view class="detail-audio-progress-thumb" :style="{ left: `${detailAudioProgressPercent}%` }"></view>
          </view>
        </view>
      </view>

      <view v-if="isLocationActionVisible" class="action-bar">
        <view
          class="location-btn"
          :class="{ disabled: !hallLocationId }"
          @tap="handleNavigate"
        >
          <svg class="location-icon" width="19" height="19" viewBox="0 0 24 24" fill="none">
            <path d="M12 21C12 21 18 14.9 18 9.8C18 6.5 15.3 4 12 4C8.7 4 6 6.5 6 9.8C6 14.9 12 21 12 21Z" stroke="currentColor" stroke-width="1.7"/>
            <circle cx="12" cy="10" r="2.2" stroke="currentColor" stroke-width="1.7"/>
          </svg>
          <text class="location-text">查看位置</text>
        </view>
      </view>
    </view>
  </GuidePageFrame>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import type { AudioItem } from '@/components/audio/AudioPlayer.vue'
import { useGlobalAudioPlayer } from '@/composables/useGlobalAudioPlayer'
import {
  explainUseCase
} from '@/usecases/explainUseCase'
import {
  guideUseCase
} from '@/usecases/guideUseCase'
import {
  toExplainDetailPageViewModel,
  type ExplainDetailPageViewModel
} from '@/view-models/explainViewModels'
import type {
  AudioPlayTargetType
} from '@/domain/museum'
import type {
  AudioLanguage
} from '@/repositories/AudioPlayInfoRepository'
import {
  isGuideTopTab,
  type GuideTopTab
} from '@/utils/guideTopTabs'
import {
  EXPLAIN_DETAIL_PLACEHOLDER_IMAGE
} from '@/utils/placeholders'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
import { normalizeGuideAudioLanguage } from '@/data/adapters/guideStopInfoAdapter'

const defaultDetail: ExplainDetailPageViewModel = {
  id: '',
  title: '讲解内容',
  subtitle: '位置待补充',
  contentType: 'exhibit',
  coverImages: [],
  summary: '该讲解内容待补充。',
  body: '该讲解内容待补充。',
  audio: {
    status: 'unavailable',
    unavailableReason: '当前语言暂无语音讲解。'
  },
  chapters: [],
  relatedItems: []
}

const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
const globalAudioPlayer = useGlobalAudioPlayer()
const activeTopTab = ref<GuideTopTab>('explain')
const resolvedHallId = ref('')
const retryingAudio = ref(false)
const languageSwitchLoading = ref(false)
const selectedAudioLanguage = ref<AudioLanguage>('zh-CN')
let languageSwitchSequence = 0
const detailEntryRequest = ref<{
  exhibitId: string
  targetType?: AudioPlayTargetType
  targetId?: string
} | null>(null)
const detailTextByLanguage = ref<Record<AudioLanguage, string>>({
  'zh-CN': defaultDetail.body,
  'yue-HK': '当前语言暂无讲解词。',
  'en-US': 'English narration text is not available yet.'
})
const detailTextLoadingByLanguage = ref<Record<AudioLanguage, boolean>>({
  'zh-CN': false,
  'yue-HK': false,
  'en-US': false
})
const detailTextLoadedByLanguage = ref<Record<AudioLanguage, boolean>>({
  'zh-CN': false,
  'yue-HK': false,
  'en-US': false
})
// 讲解详情页位置入口暂未开放，避免误导用户进入位置预览流程。
const isLocationActionVisible = false
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())

const contentLanguageOptions: Array<{
  value: AudioLanguage
  label: string
}> = [
  {
    value: 'zh-CN',
    label: '中文'
  },
  {
    value: 'yue-HK',
    label: '粤语'
  },
  {
    value: 'en-US',
    label: 'English'
  }
]

const isAudioLanguage = (value: unknown): value is AudioLanguage => (
  value === 'zh-CN' || value === 'yue-HK' || value === 'en-US'
)

const audioLanguageLabel = (lang: AudioLanguage) => {
  if (lang === 'en-US') return 'English'
  if (lang === 'yue-HK') return '粤语'
  return '中文'
}

const isRealHeroImage = (image?: string) => (
  Boolean(image && image !== EXPLAIN_DETAIL_PLACEHOLDER_IMAGE)
)

const heroImage = computed(() => exhibit.value.coverImages.find(isRealHeroImage))
const displayTitle = computed(() => {
  const withoutExplainType = exhibit.value.title
    .replace(/\s*(?:(?:\[|［|【)\s*)?标准解说\s*(?:(?:\]|］|】))?\s*$/, '')
    .trim()
  const withoutWrappingBrackets = withoutExplainType
    .replace(/^(?:\[|［|【)\s*(.*?)\s*(?:\]|］|】)$/, '$1')
    .trim()

  return withoutWrappingBrackets || withoutExplainType || exhibit.value.title
})
const detailMeta = computed(() => [
  exhibit.value.hallName,
  exhibit.value.floorLabel
].filter(Boolean).join(' · '))
const hallLocationId = computed(() => exhibit.value.hallId || resolvedHallId.value)
const heroSubtitle = computed(() => [
  detailMeta.value
].filter(Boolean).join(' · '))
const currentAudioTarget = computed(() => ({
  targetType: exhibit.value.audio.playTargetType || 'ITEM',
  targetId: exhibit.value.audio.playTargetId || exhibit.value.id,
  lang: selectedAudioLanguage.value
}))
const isCurrentDetailAudio = computed(() => globalAudioPlayer.isCurrentSource(currentAudioTarget.value))
const isCurrentDetailAudioTarget = computed(() => {
  const source = globalAudioPlayer.currentSource.value
  const targetType = exhibit.value.audio.playTargetType || 'ITEM'
  const targetId = exhibit.value.audio.playTargetId || exhibit.value.id

  return Boolean(
    globalAudioPlayer.currentAudio.value
    && source?.targetType === targetType
    && source.targetId === targetId
  )
})
const audioDockTitle = computed(() => displayTitle.value || '讲解内容')
const formatDetailAudioTime = (seconds?: number) => {
  const normalizedSeconds = Number.isFinite(seconds || 0)
    ? Math.max(0, Math.floor(seconds || 0))
    : 0
  const minutes = Math.floor(normalizedSeconds / 60)
  const remainSeconds = normalizedSeconds % 60

  return `${minutes}:${remainSeconds.toString().padStart(2, '0')}`
}
const detailAudioDurationSeconds = computed(() => {
  if (!isCurrentDetailAudioTarget.value) return exhibit.value.audio.duration || 0

  return globalAudioPlayer.duration.value
    || globalAudioPlayer.currentAudio.value?.duration
    || exhibit.value.audio.duration
    || 0
})
const detailAudioCurrentSeconds = computed(() => (
  isCurrentDetailAudioTarget.value ? globalAudioPlayer.currentTime.value : 0
))
const detailAudioTimeLabel = computed(() => (
  `${formatDetailAudioTime(detailAudioCurrentSeconds.value)} / ${formatDetailAudioTime(detailAudioDurationSeconds.value)}`
))
const audioDockSubtitle = computed(() => [
  audioLanguageLabel(selectedAudioLanguage.value),
  audioDockState.value === 'failed'
    ? globalAudioPlayer.error.value || '音频暂时无法播放'
    : audioDockState.value === 'unavailable'
      ? audioAvailabilityMessage.value
      : audioDockState.value === 'loading'
        ? '正在加载音频'
        : detailAudioTimeLabel.value
].filter(Boolean).join(' · ') || '讲解')
const detailAudioPlaying = computed(() => (
  isCurrentDetailAudio.value
  && globalAudioPlayer.playing.value
))
const detailAudioActionLabel = computed(() => (
  detailAudioPlaying.value ? '暂停讲解' : '播放讲解'
))
const detailAudioProgressPercent = computed(() => {
  if (!isCurrentDetailAudioTarget.value) return 0

  const total = detailAudioDurationSeconds.value
  if (!total) return 0

  return Math.max(0, Math.min(100, (detailAudioCurrentSeconds.value / total) * 100))
})
const isCurrentDetailAudioError = computed(() => {
  const source = globalAudioPlayer.currentSource.value
  return Boolean(
    globalAudioPlayer.error.value
    && source?.targetType === currentAudioTarget.value.targetType
    && source.targetId === currentAudioTarget.value.targetId
  )
})
const isDetailAudioLoading = computed(() => (
  languageSwitchLoading.value
  || retryingAudio.value
  || (isCurrentDetailAudioTarget.value && globalAudioPlayer.loading.value)
))
const audioDockState = computed<'playable' | 'loading' | 'unavailable' | 'failed'>(() => {
  if (isDetailAudioLoading.value) return 'loading'
  if (isCurrentDetailAudioError.value) return 'failed'
  if (exhibit.value.audio.status !== 'playable') return 'unavailable'
  return 'playable'
})
// 讲解详情页只定位到所属展厅，不再追踪具体展品点位。
const resolveHallGuidePoi = async () => {
  return guideUseCase.resolveContentLocationPreviewTarget({
    directPoiId: exhibit.value.location?.poiId,
    location: exhibit.value.location,
    hallId: exhibit.value.hallId,
    hallName: exhibit.value.hallName,
    targetName: exhibit.value.title
  })
}

const fallbackTextForLanguage = (lang: AudioLanguage) => (
  lang === 'en-US'
    ? 'English narration text is not available yet.'
    : '当前语言暂无讲解词。'
)

const applyDetailText = (lang: AudioLanguage, text?: string) => {
  const normalized = (text || '').trim()
  detailTextByLanguage.value = {
    ...detailTextByLanguage.value,
    [lang]: normalized || fallbackTextForLanguage(lang)
  }
  detailTextLoadedByLanguage.value = {
    ...detailTextLoadedByLanguage.value,
    [lang]: true
  }
}

const buildTextSourceFromViewModel = (viewModel: ExplainDetailPageViewModel) => ({
  id: viewModel.id,
  name: viewModel.title,
  hallId: viewModel.hallId,
  hallName: viewModel.hallName,
  floorId: viewModel.floorId,
  floorLabel: viewModel.floorLabel,
  image: viewModel.coverImages[0],
  description: viewModel.summary,
  guideText: viewModel.summary,
  audioLanguage: viewModel.audio.language,
  audioHasText: viewModel.audio.hasText,
  playTargetType: viewModel.audio.playTargetType,
  playTargetId: viewModel.audio.playTargetId
})

const loadFullDetailTextForLanguage = async (
  request: NonNullable<typeof detailEntryRequest.value>,
  lang: AudioLanguage,
  preferredViewModel?: ExplainDetailPageViewModel
) => {
  detailTextLoadingByLanguage.value = {
    ...detailTextLoadingByLanguage.value,
    [lang]: true
  }

  try {
    const viewModel = preferredViewModel || toExplainDetailPageViewModel(await explainUseCase.enterExplainDetail({
      ...request,
      lang
    }))

    if (viewModel.audio.hasText) {
      const selection = await explainUseCase.loadExplainDetailText(buildTextSourceFromViewModel(viewModel))
      if (selection.available) {
        const nextViewModel = toExplainDetailPageViewModel(selection.exhibit)
        applyDetailText(lang, nextViewModel.body || nextViewModel.summary)
        return
      }
    }

    applyDetailText(lang, viewModel.body || viewModel.summary)
  } catch (error) {
    console.warn('讲解正文加载失败:', lang, error)
    applyDetailText(lang)
  } finally {
    detailTextLoadingByLanguage.value = {
      ...detailTextLoadingByLanguage.value,
      [lang]: false
    }
  }
}

const loadExplainDetail = async (
  request: NonNullable<typeof detailEntryRequest.value>,
  lang: AudioLanguage
) => {
  const exhibitData = await explainUseCase.enterExplainDetail({
    ...request,
    lang
  })

  exhibit.value = toExplainDetailPageViewModel(exhibitData)
  selectedAudioLanguage.value = lang

  if (exhibitData.hallId) {
    resolvedHallId.value = exhibitData.hallId
  }

  void loadFullDetailTextForLanguage(request, lang, exhibit.value)
}

onLoad(async (options: any = {}) => {
  const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
  if (isGuideTopTab(tab)) {
    activeTopTab.value = tab
  }

  const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
  if (!exhibitId) return

  const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
  const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM'
    ? 'ITEM'
    : rawTargetType === 'STOP' || rawTargetType === 'GUIDE_STOP'
      ? 'STOP'
      : undefined
  const rawTargetId = Array.isArray(options.targetId) ? options.targetId[0] : options.targetId
  const targetId = rawTargetType === 'GUIDE_STOP'
    ? String(exhibitId)
    : rawTargetId ? String(rawTargetId) : undefined
  const rawLang = Array.isArray(options.lang) ? options.lang[0] : options.lang
  const lang = typeof rawLang === 'string'
    ? normalizeGuideAudioLanguage(rawLang)
    : 'zh-CN'

  try {
    detailEntryRequest.value = {
      exhibitId,
      targetType,
      targetId
    }
    await loadExplainDetail(detailEntryRequest.value, lang)
  } catch (error) {
    console.error('讲解详情加载失败:', error)
    uni.showToast({
      title: '讲解详情加载失败，请稍后重试',
      icon: 'none'
    })
  }
})

onUnload(() => {
  globalAudioPlayer.close()
})

const detailTextFor = (lang: AudioLanguage) => {
  return detailTextByLanguage.value[lang] || fallbackTextForLanguage(lang)
}

const splitDetailTextIntoParagraphs = (text: string): string[] => {
  const normalizedText = text
    .replace(/\r\n?/g, '\n')
    .replace(/\u3000/g, ' ')
    .trim()

  if (!normalizedText) return []

  const numberedPrefixPattern = '(?:[一二三四五六七八九十]+、|（[一二三四五六七八九十]+）|[0-9]{1,2}[、.](?!\\d))'
  const textWithNumberBreaks = normalizedText
    .replace(new RegExp(`([。！？!?；;])\\s*(?=${numberedPrefixPattern})`, 'g'), '$1\n')
    .replace(new RegExp(`\\s+(?=${numberedPrefixPattern})`, 'g'), '\n')

  return textWithNumberBreaks
    .split(/\n+/)
    .flatMap((block) => {
      const cleanedBlock = block.replace(/\s+/g, ' ').trim()
      if (!cleanedBlock) return []

      const sentences = cleanedBlock.match(/[^。！？!?；;]+[。！？!?；;]?/g)
      if (!sentences || sentences.length <= 1 || cleanedBlock.length <= 90) {
        return [cleanedBlock]
      }

      const paragraphs: string[] = []
      let currentParagraph = ''

      sentences.forEach((sentence) => {
        const cleanedSentence = sentence.trim()
        if (!cleanedSentence) return

        if (currentParagraph && `${currentParagraph}${cleanedSentence}`.length > 90) {
          paragraphs.push(currentParagraph)
          currentParagraph = cleanedSentence
          return
        }

        currentParagraph = `${currentParagraph}${cleanedSentence}`
      })

      if (currentParagraph) {
        paragraphs.push(currentParagraph)
      }

      return paragraphs
    })
}

const currentDetailText = computed(() => detailTextFor(selectedAudioLanguage.value))
const currentDetailParagraphs = computed(() => splitDetailTextIntoParagraphs(currentDetailText.value))
const currentDetailTextLoading = computed(() => (
  languageSwitchLoading.value || detailTextLoadingByLanguage.value[selectedAudioLanguage.value]
))
const detailLoadingMessage = computed(() => (
  languageSwitchLoading.value
    ? `正在切换到${audioLanguageLabel(selectedAudioLanguage.value)}`
    : '正在加载讲解正文'
))
const audioAvailabilityMessage = computed(() => (
  exhibit.value.audio.status === 'playable'
    ? ''
    : exhibit.value.audio.unavailableReason || '当前语言暂无语音讲解'
))

const replaceDetailRouteLanguage = (lang: AudioLanguage) => {
  if (typeof window === 'undefined') return

  const nextHash = `#${buildDetailRoute(lang)}`
  if (window.location.hash === nextHash) return

  window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${nextHash}`)
}

const handleLanguageChange = async (lang: AudioLanguage, options: { syncAudio?: boolean } = {}) => {
  if (lang === selectedAudioLanguage.value || languageSwitchLoading.value) return

  const shouldSyncAudio = options.syncAudio !== false && isCurrentDetailAudioTarget.value
  const request = detailEntryRequest.value
  const requestSequence = ++languageSwitchSequence
  selectedAudioLanguage.value = lang
  replaceDetailRouteLanguage(lang)
  languageSwitchLoading.value = true

  const audioSwitch = shouldSyncAudio && globalAudioPlayer.currentSource.value?.lang !== lang
    ? globalAudioPlayer.switchLanguage(lang)
    : Promise.resolve(true)

  try {
    if (!request) {
      applyDetailText(lang)
      await audioSwitch
      return
    }

    const exhibitData = await explainUseCase.enterExplainDetail({
      ...request,
      lang
    })
    if (requestSequence !== languageSwitchSequence) return

    const nextViewModel = toExplainDetailPageViewModel(exhibitData)
    exhibit.value = nextViewModel
    if (exhibitData.hallId) {
      resolvedHallId.value = exhibitData.hallId
    }

    void loadFullDetailTextForLanguage(request, lang, nextViewModel)
    await audioSwitch
  } catch (error) {
    if (requestSequence !== languageSwitchSequence) return

    console.warn('讲解语言切换失败:', lang, error)
    exhibit.value = {
      ...exhibit.value,
      audio: {
        ...exhibit.value.audio,
        status: 'unavailable',
        url: undefined,
        duration: undefined,
        language: lang,
        unavailableReason: '讲解服务暂不可用，请稍后重试'
      }
    }
    applyDetailText(lang)
    uni.showToast({
      title: '讲解语言切换失败，请稍后重试',
      icon: 'none'
    })
  } finally {
    if (requestSequence === languageSwitchSequence) {
      languageSwitchLoading.value = false
    }
  }
}

watch(
  () => globalAudioPlayer.currentSource.value,
  (source) => {
    const lang = source?.lang
    const targetType = exhibit.value.audio.playTargetType || 'ITEM'
    const targetId = exhibit.value.audio.playTargetId || exhibit.value.id

    if (
      isAudioLanguage(lang)
      && source?.targetType === targetType
      && source.targetId === targetId
    ) {
      void handleLanguageChange(lang, { syncAudio: false })
    }
  }
)

function buildDetailRoute(lang: AudioLanguage = selectedAudioLanguage.value) {
  const request = detailEntryRequest.value
  const params = new URLSearchParams({
    id: request?.exhibitId || exhibit.value.id,
    tab: activeTopTab.value
  })
  if (currentAudioTarget.value.targetType) {
    params.set('targetType', currentAudioTarget.value.targetType)
  }
  if (currentAudioTarget.value.targetId) {
    params.set('targetId', currentAudioTarget.value.targetId)
  }
  if (lang) {
    params.set('lang', lang)
  }
  return `/pages/exhibit/detail?${params.toString()}`
}

const toAudioItem = (
  selection: NonNullable<Awaited<ReturnType<typeof explainUseCase.selectAudioForExplainDetail>>>
): AudioItem | null => {
  if (!selection.playable || !selection.media?.url) return null

  return {
    id: selection.media.id,
    name: selection.playInfo?.title || selection.exhibit.name,
    audioUrl: selection.media.url,
    image: heroImage.value,
    duration: selection.media.duration,
    language: selection.media.language as AudioLanguage,
    supportedLanguages: (selection.exhibit.supportedLanguages || exhibit.value.audio.supportedLanguages || [])
      .filter(isAudioLanguage)
  }
}

const refreshCurrentAudioOnce = async (message: string) => {
  if (retryingAudio.value || exhibit.value.audio.status !== 'playable') return null

  retryingAudio.value = true
  const selection = await explainUseCase.selectAudioForExplainDetail({
    id: exhibit.value.id,
    name: exhibit.value.title,
    image: heroImage.value,
    description: exhibit.value.summary,
    guideText: exhibit.value.summary,
    audioUrl: exhibit.value.audio.url,
    audioDuration: exhibit.value.audio.duration,
    audioStatus: 'READY',
    audioLanguage: selectedAudioLanguage.value,
    playTargetType: exhibit.value.audio.playTargetType,
    playTargetId: exhibit.value.audio.playTargetId
  }, {
    refreshPlayInfo: true
  })

  retryingAudio.value = false
  const retryAudio = toAudioItem(selection)
  if (retryAudio) return retryAudio

  exhibit.value = {
    ...exhibit.value,
    audio: {
      ...exhibit.value.audio,
      status: 'unavailable',
      url: undefined,
      unavailableReason: selection.unavailableMessage || message || '音频加载失败，当前提供图文讲解。'
    }
  }
  return null
}

const playDetailAudio = async (audio: AudioItem) => {
  await globalAudioPlayer.play(audio, {
    source: {
      exhibitId: exhibit.value.id,
      targetType: currentAudioTarget.value.targetType,
      targetId: currentAudioTarget.value.targetId,
      lang: selectedAudioLanguage.value,
      title: audio.name,
      detailRoute: buildDetailRoute()
    },
    retryOnError: refreshCurrentAudioOnce,
    displayMode: 'mini'
  })
}

const handlePlayAudio = async (options: { forceRefresh?: boolean } = {}) => {
  if (audioDockState.value === 'unavailable') return

  if (isCurrentDetailAudio.value && !options.forceRefresh) {
    if (globalAudioPlayer.playing.value) {
      globalAudioPlayer.pause()
      return
    }

    void globalAudioPlayer.resume()
    return
  }

  const refreshedAudio = options.forceRefresh
    ? await refreshCurrentAudioOnce(globalAudioPlayer.error.value)
    : null
  if (refreshedAudio) {
    await playDetailAudio(refreshedAudio)
    return
  }

  const selection = exhibit.value.id
    ? await explainUseCase.selectAudioForExplainDetail({
      id: exhibit.value.id,
      name: exhibit.value.title,
      image: heroImage.value,
      description: exhibit.value.summary,
      guideText: exhibit.value.summary,
      audioUrl: exhibit.value.audio.url,
      audioDuration: exhibit.value.audio.duration,
      audioStatus: exhibit.value.audio.status === 'playable' ? 'READY' : 'MISSING',
      audioLanguage: selectedAudioLanguage.value,
      audioUnavailableReason: exhibit.value.audio.unavailableReason,
      playTargetType: exhibit.value.audio.playTargetType,
      playTargetId: exhibit.value.audio.playTargetId
    })
    : null

  if (!selection?.playable || !selection.media?.url) {
    uni.showToast({
      title: selection?.unavailableMessage || exhibit.value.audio.unavailableReason || '该讲解暂无可播放音频',
      icon: 'none'
    })
    return
  }

  const audio = toAudioItem(selection)
  if (!audio) return

  await playDetailAudio(audio)
}

const handleRetryAudio = async () => {
  await handlePlayAudio({ forceRefresh: true })
}

const handleSeekAudio = (event: { detail?: { x?: number }; currentTarget?: { offsetWidth?: number } }) => {
  if (audioDockState.value !== 'playable' || !detailAudioDurationSeconds.value) return

  const width = event.currentTarget?.offsetWidth || 0
  const x = event.detail?.x
  if (!width || typeof x !== 'number') return
  globalAudioPlayer.seekToPercent(Math.max(0, Math.min(100, x / width * 100)))
}

const handleNavigate = async () => {
  const matchedHallPoi = await resolveHallGuidePoi()
  if (!matchedHallPoi) {
    uni.showToast({
      title: '该讲解暂无所属展厅位置数据',
      icon: 'none'
    })
    return
  }

  resolvedHallId.value = matchedHallPoi.id
  exhibit.value = {
    ...exhibit.value,
    hallId: matchedHallPoi.id,
    hallName: matchedHallPoi.hallName || matchedHallPoi.name,
    location: exhibit.value.location || {
      status: 'hallFallback',
      poiId: matchedHallPoi.id,
      sourcePoiId: matchedHallPoi.id,
      actionText: '查看所属展厅',
      previewOnly: true,
      floorId: matchedHallPoi.floorId,
      floorLabel: matchedHallPoi.floorLabel,
      note: '已定位到该讲解所属展厅。'
    }
  }

  uni.navigateTo({
    url: `/pages/route/detail?facilityId=${encodeURIComponent(matchedHallPoi.id)}&target=${encodeURIComponent(exhibit.value.hallName || exhibit.value.title)}&state=preview`
  })
}

const fallbackToExplainHome = () => {
  uni.redirectTo({
    url: '/pages/index/index?tab=explain',
    fail: () => {
      uni.reLaunch({
        url: '/pages/index/index?tab=explain'
      })
    }
  })
}

const handleBack = () => {
  uni.navigateBack({
    delta: 1,
    fail: fallbackToExplainHome
  })
}
</script>

<style scoped lang="scss">
.explain-detail-page {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #f7f8f3;
}

.content {
  position: absolute;
  top: 392px;
  right: 0;
  bottom: 0;
  left: 0;
  height: auto;
  padding: 0;
  box-sizing: border-box;
  background: #f7f8f3;
}

.detail-language-bar {
  position: absolute;
  top: 320px;
  right: 0;
  left: 0;
  z-index: 2;
  height: 72px;
  padding: 25px 24px 7px;
  box-sizing: border-box;
  background: #f7f8f3;
}

.immersive-hero {
  position: relative;
  min-height: 0;
  height: 320px;
  padding: calc(env(safe-area-inset-top) + 0px) 24px 24px;
  box-sizing: border-box;
  overflow: hidden;
  background: #111a14;
  color: #ffffff;
}

.immersive-hero.has-real-image {
  background: #111a14;
}

.hero-image-shade,
.immersive-hero.has-real-image .hero-image-shade {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.08) 42%, rgba(0, 0, 0, 0.58) 100%);
}

.hero-real-image {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
}

.hero-back {
  position: relative;
  z-index: 2;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #ffffff;
}

.hero-back::after {
  border: 0;
}

.hero-back-icon {
  margin-top: -3px;
  font-size: 32px;
  line-height: 32px;
  font-weight: 400;
}

.hero-copy {
  position: relative;
  z-index: 2;
  min-height: 0;
  height: calc(100% - 44px - env(safe-area-inset-top));
  padding-top: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.detail-title {
  display: block;
  max-width: 100%;
  font-size: 28px;
  line-height: 34px;
  font-weight: 800;
  letter-spacing: 0;
  color: #ffffff;
  text-shadow: none;
  word-break: break-word;
}

.detail-meta {
  display: block;
  margin-top: 6px;
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.86);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.detail-chip-row { display: none; }

.detail-content {
  min-height: calc(100vh - 392px - 72px - env(safe-area-inset-bottom));
  padding: 24px 24px calc(32px + 72px + env(safe-area-inset-bottom));
  box-sizing: border-box;
  background: #f7f8f3;
}

.detail-audio-play {
  position: relative;
  width: 44px;
  height: 44px;
  min-width: 44px;
  flex: 0 0 44px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: #e8e800;
  color: #141412;
}

.detail-audio-play svg {
  display: block;
  flex: none;
}

.detail-audio-play svg path {
  transform: translateX(1px);
}

.detail-audio-play::after,
.language-option::after {
  border: 0;
}

.language-switch {
  height: 40px;
  margin-top: 0;
  padding: 0;
  display: flex;
  box-sizing: border-box;
  background: transparent;
  border: 1px solid #d8ddd2;
  border-radius: 6px;
}

.language-option {
  position: relative;
  min-width: 0;
  flex: 1;
  height: 40px;
  padding: 0 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: #5d6659;
}

.language-option + .language-option {
  border-left: 1px solid #d8ddd2;
}

.language-option.active {
  background: #e8e800;
  color: #141412;
}

.language-option.active::before {
  position: absolute;
  right: 10px;
  bottom: 1px;
  left: 10px;
  height: 3px;
  content: '';
  background: #141412;
}

.language-option.disabled {
  opacity: 0.56;
}

.language-option-text {
  display: block;
  max-width: 100%;
  font-size: 14px;
  line-height: 20px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.content-section {
  margin-top: 0;
  padding-top: 0;
}

.section-text {
  display: block;
  margin-top: 0;
  font-size: 16px;
  line-height: 26px;
  color: #31392e;
  text-indent: 2em;
  word-break: break-word;
  overflow-wrap: anywhere;
}

.section-hint {
  margin-top: 14px;
  font-size: 14px;
  line-height: 22px;
}

.audio-status-hint {
  color: #7a5a22;
}

.detail-audio-dock {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 10;
  height: calc(72px + env(safe-area-inset-bottom));
  padding: 0 24px env(safe-area-inset-bottom);
  box-sizing: border-box;
  background: #ffffff;
  border-top: 1px solid #e8e5de;
}

.detail-audio-dock-main {
  height: 50px;
  padding-left: 58px;
  display: flex;
  align-items: center;
  gap: 14px;
}

.detail-audio-main {
  min-width: 0;
  flex: 1;
}

.detail-audio-title,
.detail-audio-subtitle {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.detail-audio-title {
  font-size: 14px;
  line-height: 20px;
  font-weight: 500;
  color: #141412;
}

.detail-audio-subtitle {
  margin-top: 1px;
  font-size: 12px;
  line-height: 16px;
  color: #666661;
}

.detail-audio-play.disabled {
  background: #e8e5de;
  color: #94948f;
}

.detail-audio-play {
  position: absolute;
  top: 14px;
  left: 19px;
  z-index: 1;
}

.detail-audio-retry {
  height: 32px;
  padding: 0 10px;
  flex-shrink: 0;
  border: 0;
  border-radius: 4px;
  background: #e8e800;
  color: #141412;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
}

.detail-audio-retry::after {
  border: 0;
}

.detail-audio-progress-hit {
  height: 22px;
  margin: 0 0 0 58px;
  display: flex;
  align-items: center;
}

.detail-audio-progress-hit.disabled {
  opacity: 0.42;
}

.detail-audio-progress {
  position: relative;
  width: 100%;
  height: 4px;
  overflow: visible;
  background: #94948f;
  border-radius: 2px;
}

.detail-audio-progress-fill {
  width: 0;
  height: 100%;
  background: #e0e100;
  border-radius: inherit;
}

.detail-audio-progress-thumb {
  position: absolute;
  top: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #141412;
  transform: translate(-50%, -50%);
}

.detail-audio-progress.is-indeterminate {
  width: calc(100% - 58px);
  height: 4px;
  margin: 8px 0 0 58px;
  overflow: hidden;
  background: #deded6;
}

.detail-audio-progress.is-indeterminate::after {
  display: block;
  width: 38%;
  height: 100%;
  content: '';
  background: #e0e100;
  animation: audio-indeterminate 1.1s ease-in-out infinite;
}

.audio-loading-spinner {
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border: 2px solid rgba(20, 20, 18, 0.28);
  border-top-color: #141412;
  border-radius: 50%;
  animation: audio-spinner 0.8s linear infinite;
}

@keyframes audio-spinner { to { transform: rotate(360deg); } }
@keyframes audio-indeterminate { from { transform: translateX(-110%); } to { transform: translateX(290%); } }

@media (min-width: 420px) {
  .immersive-hero {
    padding-right: 24px;
    padding-left: 24px;
  }

  .detail-content {
    padding-right: 24px;
    padding-left: 24px;
  }
}

@media (max-width: 360px) {
  .detail-title {
    font-size: 24px;
    line-height: 30px;
  }

  .detail-content {
    padding-right: 16px;
    padding-left: 16px;
  }

  .detail-audio-dock {
    padding-right: 16px;
    padding-left: 16px;
  }
}
</style>
