<template>
  <view class="report-view">
    <view v-if="loading" class="state-wrap">
      <view class="state-spinner"></view>
      <text class="state-text">加载中</text>
    </view>

    <view v-else-if="notFound" class="state-wrap">
      <text class="state-emoji">🐾</text>
      <text class="state-text">报告不存在或链接已失效</text>
    </view>

    <view v-else-if="reportData" class="report-content">
      <!-- 顶部品牌 -->
      <view class="brand-bar nav-gradient" :style="brandBarSafe">
        <view class="brand-bar-inner">
          <view class="brand-home" @click="goBackOrHome">
            <AppIcon name="back" :size="16" color="#ffffff" />
          </view>
          <text class="brand-name">{{ reportData.store?.name || '宠伴生活馆' }}</text>
          <view style="width:32px"></view>
        </view>
      </view>

      <!-- 主视觉：照片对比 -->
      <view class="hero-stage">
        <view class="hero-photos">
          <view class="hero-before">
            <image
              v-if="beforePhotos.length > 0"
              :src="imgUrl(beforePhotos[0])"
              class="hero-img"
              mode="aspectFill"
              @click="previewPhotos('before', 0)"
            />
            <view v-else class="hero-empty">
              <text class="hero-empty-text">暂无</text>
            </view>
          <view v-if="beforePhotos.length > 0" class="hero-seq hero-seq-before">
            <text>1 / {{ beforePhotos.length }}</text>
          </view>
            <view class="hero-tag tag-before">Before</view>
          </view>
          <view class="hero-after">
            <image
              v-if="afterPhotos.length > 0"
              :src="imgUrl(afterPhotos[0])"
              class="hero-img"
              mode="aspectFill"
              @click="previewPhotos('after', 0)"
            />
            <view v-else class="hero-empty">
              <text class="hero-empty-text">暂无</text>
            </view>
          <view v-if="afterPhotos.length > 0" class="hero-seq hero-seq-after">
            <text>1 / {{ afterPhotos.length }}</text>
          </view>
            <view class="hero-tag tag-after">After</view>
          </view>
        </view>
        <view class="hero-divider">
          <text class="hero-divider-text">前后对比</text>
        </view>
      </view>

      <view class="hero-meta">
        <view class="hero-meta-chip hero-meta-chip-before">
          <text>服务前 {{ beforePhotos.length }} 张</text>
        </view>
        <view class="hero-meta-chip hero-meta-chip-after">
          <text>服务后 {{ afterPhotos.length }} 张</text>
        </view>
      </view>
      <view v-if="beforePhotos.length > 0 || afterPhotos.length > 0" class="hero-tip">
        <text>点击图片查看大图</text>
      </view>
      <view v-if="beforePhotos.length > 0 || afterPhotos.length > 0" class="hero-actions">
        <view
          v-if="beforePhotos.length > 0"
          class="hero-action-chip hero-action-chip-before"
          @click="previewPhotos('before', 0)"
        >
          <text>查看服务前全部照片</text>
        </view>
        <view
          v-if="afterPhotos.length > 0"
          class="hero-action-chip hero-action-chip-after"
          @click="previewPhotos('after', 0)"
        >
          <text>查看服务后全部照片</text>
        </view>
      </view>

      <!-- 补充照片：前后分组，不混排 -->
      <view v-if="beforeMorePhotos.length > 0 || afterMorePhotos.length > 0" class="photo-groups">
        <view v-if="beforeMorePhotos.length > 0" class="photo-group-card">
          <view class="photo-group-head">
            <text class="photo-group-title">服务前补充</text>
            <text class="photo-group-count">{{ beforeMorePhotos.length }} 张</text>
          </view>
          <view class="photo-grid-extra">
            <view
              v-for="(img, idx) in beforeMorePhotos"
              :key="'before-more-' + idx"
              class="photo-grid-extra-item"
              @click="previewPhotos('before', idx + 1)"
            >
              <image
                :src="imgUrl(img)"
                class="photo-grid-extra-img"
                mode="aspectFill"
              />
              <view class="photo-grid-extra-badge">
                <text>{{ idx + 2 }}</text>
              </view>
            </view>
          </view>
        </view>

        <view v-if="afterMorePhotos.length > 0" class="photo-group-card">
          <view class="photo-group-head">
            <text class="photo-group-title">服务后补充</text>
            <text class="photo-group-count">{{ afterMorePhotos.length }} 张</text>
          </view>
          <view class="photo-grid-extra">
            <view
              v-for="(img, idx) in afterMorePhotos"
              :key="'after-more-' + idx"
              class="photo-grid-extra-item"
              @click="previewPhotos('after', idx + 1)"
            >
              <image
                :src="imgUrl(img)"
                class="photo-grid-extra-img"
                mode="aspectFill"
              />
              <view class="photo-grid-extra-badge photo-grid-extra-badge-after">
                <text>{{ idx + 2 }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>

      <!-- 服务过程中（照片+视频） -->
      <view v-if="duringMedia.length > 0" class="section-gap">
        <view class="during-section">
          <view class="during-title">
            <view class="during-title-dot"></view>
            <text>服务过程中</text>
          </view>

          <!-- 照片 -->
          <view v-if="duringPhotoItems.length > 0" class="during-group">
            <view class="during-group-label">照片 {{ duringPhotoItems.length }} 张</view>
            <view class="during-grid">
              <view
                v-for="(url, idx) in duringPhotoItems"
                :key="'dp-' + idx"
                class="during-thumb"
                @click="previewPhotos('during-photo', idx)"
              >
                <image :src="imgUrl(url)" class="during-thumb-img" mode="aspectFill" />
              </view>
            </view>
          </view>

          <!-- 视频 -->
          <view v-if="duringVideoItems.length > 0" class="during-group">
            <view class="during-group-label">视频 {{ duringVideoItems.length }} 个</view>
            <view class="during-grid">
              <view
                v-for="(url, idx) in duringVideoItems"
                :key="'dv-' + idx"
                class="during-thumb during-thumb--video"
                @click="playVideo(imgUrl(url))"
              >
                <video
                  :src="imgUrl(url)"
                  class="during-thumb-img"
                  :show-center-play-btn="true"
                  :show-play-btn="true"
                  objectFit="cover"
                  :enable-progress-gesture="false"
                />
              </view>
            </view>
          </view>
        </view>
      </view>

      <!-- 宠主问候 -->
      <view v-if="!isStaff" class="greeting-card">
        <text class="greeting-text">
          <text class="greeting-pet">{{ reportData.petName }}</text> 的服务已完成，看看TA的变化吧 ✨
        </text>
      </view>

      <!-- 服务信息 -->
      <view class="info-card">
        <view class="info-title">服务详情</view>
        <view class="info-row">
          <text class="info-key">宠物</text>
          <text class="info-val">{{ reportData.petName }}</text>
        </view>
        <view class="info-row">
          <text class="info-key">项目</text>
          <text class="info-val">{{ reportData.serviceType }}</text>
        </view>
        <view class="info-row">
          <text class="info-key">时间</text>
          <text class="info-val">{{ formatTime(reportData.appointmentTime) }}</text>
        </view>
        <view class="info-row" v-if="reportData.staffName">
          <text class="info-key">技师</text>
          <text class="info-val">{{ reportData.staffName }}</text>
        </view>
      </view>

      <!-- 备注 -->
      <view v-if="reportData.remark" class="remark-card">
        <text class="remark-label">备注</text>
        <text class="remark-text">{{ reportData.remark }}</text>
      </view>

      <!-- 服务回顾短片：素材智能剪辑（后端可扩展接入生成式 AI 视频） -->
      <view v-if="hasHighlightSource" class="highlight-card">
        <view class="highlight-head">
          <text class="highlight-title">服务回顾短片</text>
          <text class="highlight-badge">智能剪辑</text>
        </view>
        <text class="highlight-desc">
          按「服务前 → 过程 → 服务后」顺序，将照片与短视频自动合成竖屏 9:16 成片（15 秒或 30 秒）。门店服务器需已安装 ffmpeg；若需大模型生成式成片，可在后端接入对应云 API。
        </text>
        <view v-if="highlightStatus === 'processing'" class="highlight-status highlight-status--busy">
          <text>短片生成中，请稍候…</text>
        </view>
        <view v-else-if="highlightStatus === 'failed'" class="highlight-status highlight-status--err">
          <text>{{ reportData.highlightVideoError || '生成失败' }}</text>
        </view>
        <video
          v-if="highlightReady"
          class="highlight-player"
          :src="imgUrl(reportData.highlightVideoUrl)"
          controls
          :show-center-play-btn="true"
          objectFit="contain"
        />
        <view v-if="canTriggerHighlight" class="highlight-actions">
          <button
            class="highlight-btn"
            :disabled="highlightStatus === 'processing'"
            @click="startHighlight(15)"
          >
            生成 15 秒
          </button>
          <button
            class="highlight-btn highlight-btn--secondary"
            :disabled="highlightStatus === 'processing'"
            @click="startHighlight(30)"
          >
            生成 30 秒
          </button>
        </view>
      </view>

      <!-- 操作区 -->
      <view class="actions">
        <template v-if="isStaff">
          <button open-type="share" class="app-btn-main app-btn-primary">
            转发给宠主
          </button>
          <button class="app-btn-main app-btn-soft" @click="openPosterModal">
            保存海报到相册
          </button>
        </template>
        <template v-else>
          <button class="app-btn-main app-btn-primary" @click="callStore">
            联系门店
          </button>
          <button v-if="reportData.store?.latitude" class="app-btn-main app-btn-soft" @click="navToStore">
            导航去门店
          </button>
          <button class="app-btn-main app-btn-outline action-link-btn" @click="goHome">
            我也要预约
          </button>
        </template>
      </view>

      <!-- 底部品牌 -->
      <view class="footer-brand">
        <text class="footer-line"></text>
        <text class="footer-name">{{ reportData.store?.name || '宠伴生活馆' }}</text>
        <text class="footer-sub">宠物服务 · 让爱更专业</text>
      </view>
    </view>

    <TestimonialModal
      v-if="isStaff"
      v-model="posterModalOpen"
      :pet-name="reportData?.petName"
      @skip="onTestimonialSkip"
      @confirm="onTestimonialConfirm"
    />

    <!-- #ifdef H5 -->
    <canvas ref="posterCanvas" class="poster-offscreen" />
    <!-- #endif -->
    <!-- #ifdef MP-WEIXIN -->
    <canvas id="reportPosterCanvas" type="2d" class="poster-offscreen" />
    <!-- #endif -->
  </view>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, getCurrentInstance, nextTick } from 'vue'
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { getReportByToken, imgUrl, postReportHighlightStart, submitReportTestimonial } from '../../api/index.js'
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../../utils/reportPosterDraw.js'
import AppIcon from '../../components/AppIcon.vue'
import TestimonialModal from '../../components/report/TestimonialModal.vue'
import { getUserSession, isLoggedIn } from '../../utils/session.js'

const loading = ref(true)
const notFound = ref(false)
const reportData = ref(null)
const posterCanvas = ref(null)
const posterModalOpen = ref(false)
const routeToken = ref('')

const isStaff = computed(() => isLoggedIn())

const hasHighlightSource = computed(() => {
  const r = reportData.value
  if (!r) return false
  const b = r.beforePhotos?.length || 0
  const a = r.afterPhotos?.length || 0
  const d = r.duringMedia?.length || 0
  return b + a + d > 0
})

const highlightStatus = computed(() => reportData.value?.highlightVideoStatus || '')

const highlightReady = computed(
  () => highlightStatus.value === 'done' && !!reportData.value?.highlightVideoUrl
)

/** 仅店长/员工在登录态下可发起合成（宠主仅查看已生成短片） */
const canTriggerHighlight = computed(() => {
  const u = getUserSession()
  const role = u?.role
  return !!(u?.id && (role === 'boss' || role === 'staff'))
})

let highlightPollTimer = null

const stopHighlightPoll = () => {
  if (highlightPollTimer != null) {
    clearInterval(highlightPollTimer)
    highlightPollTimer = null
  }
}

const getRouteToken = () => {
  let token = routeToken.value
  // #ifdef H5
  token = new URLSearchParams(window.location.search).get('token') || token
  // #endif
  return token || ''
}

const maybeStartHighlightPoll = () => {
  stopHighlightPoll()
  if (reportData.value?.highlightVideoStatus !== 'processing') return
  highlightPollTimer = setInterval(async () => {
    const t = getRouteToken()
    if (!t) {
      stopHighlightPoll()
      return
    }
    const res = await getReportByToken(t)
    if (res.code === 200 && res.data) {
      reportData.value = res.data
      if (res.data.highlightVideoStatus !== 'processing') {
        stopHighlightPoll()
        if (res.data.highlightVideoStatus === 'done') {
          uni.showToast({ title: '短片已生成', icon: 'success' })
        }
      }
    }
  }, 2500)
}

const startHighlight = async (durationSec) => {
  const u = getUserSession()
  if (!reportData.value?.id || !u?.id) {
    uni.showToast({ title: '请先登录门店账号', icon: 'none' })
    return
  }
  uni.showLoading({ title: '提交中' })
  const res = await postReportHighlightStart({
    reportId: reportData.value.id,
    durationSec,
    operatorUserId: u.id,
    role: u.role || 'staff'
  })
  uni.hideLoading()
  if (res.code === 200) {
    uni.showToast({ title: res.message || '已开始生成', icon: 'none' })
    reportData.value = { ...reportData.value, highlightVideoStatus: 'processing' }
    maybeStartHighlightPoll()
  } else if (res.code === 503) {
    uni.showModal({
      title: '暂无法生成',
      content: res.message || '服务器未配置 ffmpeg',
      showCancel: false
    })
  } else {
    uni.showToast({ title: res.message || '发起失败', icon: 'none' })
  }
}

const beforePhotos = computed(() => {
  if (!reportData.value?.beforePhotos) return []
  // 支持新旧两种格式：旧版是 string[]，新版是 {url, mediaType}[]
  const raw = reportData.value.beforePhotos
  if (raw.length === 0) return []
  if (typeof raw[0] === 'string') return raw
  return raw.map(item => item.url)
})
const afterPhotos = computed(() => {
  if (!reportData.value?.afterPhotos) return []
  const raw = reportData.value.afterPhotos
  if (raw.length === 0) return []
  if (typeof raw[0] === 'string') return raw
  return raw.map(item => item.url)
})
const duringMedia = computed(() => {
  return reportData.value?.duringMedia || []
})
const duringPhotoItems = computed(() => duringMedia.value.filter(i => i.mediaType !== 'video').map(i => i.url))
const duringVideoItems = computed(() => duringMedia.value.filter(i => i.mediaType === 'video').map(i => i.url))
const beforePhotoUrls = computed(() => beforePhotos.value.map((item) => imgUrl(item)))
const afterPhotoUrls = computed(() => afterPhotos.value.map((item) => imgUrl(item)))
const beforeMorePhotos = computed(() => beforePhotos.value.slice(1))
const afterMorePhotos = computed(() => afterPhotos.value.slice(1))

const shareTitle = computed(() => {
  const r = reportData.value
  if (!r) return '洗护前后对比'
  const store = r.store?.name || '宠伴生活馆'
  const pet = r.petName || '爱宠'
  return `${pet}的洗护蜕变｜${store}`
})

const shareCoverUrl = computed(() => {
  if (afterPhotos.value.length > 0) return imgUrl(afterPhotos.value[0])
  if (beforePhotos.value.length > 0) return imgUrl(beforePhotos.value[0])
  return ''
})

const brandBarSafe = (() => {
  const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
  return `padding-top:${statusBarHeight + 8}px;`
})()

const formatTime = (time) => formatDateTimeYMDHM(time)

const loadReport = async () => {
  const token = getRouteToken()
  if (!token) {
    notFound.value = true
    loading.value = false
    return
  }
  uni.showLoading({ title: '加载中' })
  const res = await getReportByToken(token)
  uni.hideLoading()
  loading.value = false
  if (res.code === 200) {
    reportData.value = res.data
    maybeStartHighlightPoll()
  } else {
    notFound.value = true
  }
}

const goHome = () => uni.reLaunch({ url: '/pages/home/Home' })

const goBackOrHome = () => {
  try {
    const pages = getCurrentPages()
    if (pages.length > 1) {
      uni.navigateBack()
      return
    }
  } catch (_) {
    // ignore and fallback to home
  }
  goHome()
}

const callStore = () => {
  const phone = reportData.value?.store?.phone
  if (!phone) return uni.showToast({ title: '门店未设置电话', icon: 'none' })
  uni.makePhoneCall({ phoneNumber: phone })
}

const navToStore = () => {
  const store = reportData.value?.store
  if (!store || !store.latitude || !store.longitude) {
    return uni.showToast({ title: '门店未设置导航坐标', icon: 'none' })
  }
  uni.openLocation({
    latitude: Number(store.latitude),
    longitude: Number(store.longitude),
    name: store.name || '宠伴生活馆',
    address: store.address || ''
  })
}

const previewPhotos = (group, index = 0) => {
  let urls
  if (group === 'before') urls = beforePhotoUrls.value
  else if (group === 'after') urls = afterPhotoUrls.value
  else if (group === 'during-photo') urls = duringPhotoItems.value.map(imgUrl)
  else if (group === 'during-video') urls = duringVideoItems.value.map(imgUrl)
  if (!urls || !urls.length) return
  const current = urls[Math.max(0, Math.min(index, urls.length - 1))]
  uni.previewImage({
    urls,
    current
  })
}

const playVideo = (url) => {
  if (!url) return
  uni.navigateTo({
    url: `/pages/video-player/videoPlayer?url=${encodeURIComponent(url)}`
  })
}

onShareAppMessage(() => {
  const q = routeToken.value ? `token=${encodeURIComponent(routeToken.value)}` : ''
  return {
    title: shareTitle.value,
    path: `/pages/report-view/reportView${q ? '?' + q : ''}`,
    imageUrl: shareCoverUrl.value || undefined
  }
})

onShareTimeline(() => {
  const q = routeToken.value ? `token=${encodeURIComponent(routeToken.value)}` : ''
  return {
    title: shareTitle.value,
    query: q,
    imageUrl: shareCoverUrl.value || undefined
  }
})

const openPosterModal = () => {
  if (!reportData.value) return
  posterModalOpen.value = true
}

const onTestimonialSkip = () => {
  posterModalOpen.value = false
  // #ifdef H5
  runGeneratePosterH5('')
  // #endif
  // #ifdef MP-WEIXIN
  runGeneratePosterMp('')
  // #endif
  // #ifndef H5
  // #ifndef MP-WEIXIN
  uni.showToast({ title: '请截图分享报告页', icon: 'none' })
  // #endif
  // #endif
}

const onTestimonialConfirm = async (text) => {
  posterModalOpen.value = false
  const t = (text || '').trim()
  const tok = getRouteToken()
  if (t && tok) {
    try {
      const res = await submitReportTestimonial(tok, t, true)
      if (res.code !== 200) {
        uni.showToast({ title: res.message || '寄语未保存，仍将生成海报', icon: 'none' })
      }
    } catch (e) {
      uni.showToast({ title: '寄语未保存，仍将生成海报', icon: 'none' })
    }
  }
  // #ifdef H5
  await runGeneratePosterH5(t)
  // #endif
  // #ifdef MP-WEIXIN
  await runGeneratePosterMp(t)
  // #endif
  // #ifndef H5
  // #ifndef MP-WEIXIN
  uni.showToast({ title: '当前环境无法导出海报，可截图分享', icon: 'none' })
  // #endif
  // #endif
}

// #ifdef H5
const loadImageH5 = (src) => {
  return new Promise((resolve) => {
    const img = new Image()
    img.crossOrigin = 'anonymous'
    img.onload = () => resolve(img)
    img.onerror = () => resolve(null)
    img.src = src
  })
}

const runGeneratePosterH5 = async (testimonial) => {
  if (!reportData.value) return
  const r = reportData.value
  uni.showToast({ title: '正在生成海报...', icon: 'none' })
  const beforeUrl = beforePhotos.value[0] ? imgUrl(beforePhotos.value[0]) : ''
  const afterUrl = afterPhotos.value[0] ? imgUrl(afterPhotos.value[0]) : ''
  const [beforeImg, afterImg] = await Promise.all([
    beforeUrl ? loadImageH5(beforeUrl) : Promise.resolve(null),
    afterUrl ? loadImageH5(afterUrl) : Promise.resolve(null)
  ])
  const canvas = posterCanvas.value
  if (!canvas) return
  const ctx = canvas.getContext('2d')
  const posterH = computePosterLogicalHeight(false, testimonial || '')
  canvas.width = POSTER_W
  canvas.height = posterH
  drawReportPoster(ctx, {
    storeName: r.store?.name || '',
    storePhone: r.store?.phone || '',
    storeAddr: r.store?.address || '',
    petName: r.petName || '',
    serviceType: r.serviceType || '',
    timeStr: formatTime(r.appointmentTime) || '',
    staffName: r.staffName || '',
    remark: r.remark || '',
    beforeCount: beforePhotos.value.length,
    afterCount: afterPhotos.value.length,
    beforeImg,
    afterImg,
    testimonial: testimonial || ''
  })
  const link = document.createElement('a')
  link.download = `服务报告_${r.petName || '宠物'}.png`
  link.href = canvas.toDataURL('image/png')
  link.click()
  uni.showToast({ title: '已下载海报', icon: 'success' })
}
// #endif

// #ifdef MP-WEIXIN
function downloadToTemp(url) {
  return new Promise((resolve) => {
    if (!url) { resolve(null); return }
    uni.downloadFile({
      url,
      success: (res) => {
        if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath)
        else resolve(null)
      },
      fail: () => resolve(null)
    })
  })
}

function queryPosterCanvasNode() {
  return new Promise((resolve) => {
    const inst = getCurrentInstance()
    const proxy = inst?.proxy
    const pick = (res) => (res && res[0] && res[0].node ? res[0].node : null)
    const fallback = () => {
      uni.createSelectorQuery()
        .select('#reportPosterCanvas')
        .fields({ node: true, size: true })
        .exec((res2) => resolve(pick(res2)))
    }
    if (proxy) {
      uni.createSelectorQuery()
        .in(proxy)
        .select('#reportPosterCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          const n = pick(res)
          if (n) resolve(n)
          else fallback()
        })
    } else {
      fallback()
    }
  })
}

function loadCanvasImage(canvas, tempPath) {
  return new Promise((resolve) => {
    if (!tempPath) { resolve(null); return }
    const img = canvas.createImage()
    img.onload = () => resolve(img)
    img.onerror = () => resolve(null)
    img.src = tempPath
  })
}

const runGeneratePosterMp = async (testimonial) => {
  if (!reportData.value) return
  const r = reportData.value
  uni.showLoading({ title: '生成海报中...', mask: true })
  try {
    const beforeUrl = beforePhotos.value[0] ? imgUrl(beforePhotos.value[0]) : ''
    const afterUrl = afterPhotos.value[0] ? imgUrl(afterPhotos.value[0]) : ''
    const [beforePath, afterPath] = await Promise.all([
      downloadToTemp(beforeUrl),
      downloadToTemp(afterUrl)
    ])

    await nextTick()
    await new Promise((res) => setTimeout(res, 100))

    let canvas = await queryPosterCanvasNode()
    if (!canvas) {
      await new Promise((res) => setTimeout(res, 200))
      canvas = await new Promise((resolve) => {
        uni.createSelectorQuery()
          .select('#reportPosterCanvas')
          .fields({ node: true, size: true })
          .exec((res2) => resolve(res2[0]?.node || null))
      })
    }
    if (!canvas) {
      uni.showToast({ title: '画布未就绪，请稍后重试', icon: 'none' })
      return
    }

    const inst = getCurrentInstance()
    const proxy = inst?.proxy

    const ctx = canvas.getContext('2d')
    if (!ctx) {
      uni.showToast({ title: '画布上下文异常', icon: 'none' })
      return
    }
    let dpr = 2
    try {
      const win = typeof uni.getWindowInfo === 'function' ? uni.getWindowInfo() : null
      if (win && win.pixelRatio != null) dpr = Math.min(win.pixelRatio, 3)
      else dpr = Math.min(uni.getSystemInfoSync().pixelRatio || 2, 3)
    } catch (_) { dpr = 2 }
    const posterH = computePosterLogicalHeight(false, testimonial || '')
    canvas.width = POSTER_W * dpr
    canvas.height = posterH * dpr
    ctx.scale(dpr, dpr)

    const beforeImg = await loadCanvasImage(canvas, beforePath)
    const afterImg = await loadCanvasImage(canvas, afterPath)

    try {
      drawReportPoster(ctx, {
        storeName: r.store?.name || '',
        storePhone: r.store?.phone || '',
        storeAddr: r.store?.address || '',
        petName: r.petName || '',
        serviceType: r.serviceType || '',
        timeStr: formatTime(r.appointmentTime) || '',
        staffName: r.staffName || '',
        remark: r.remark || '',
        beforeCount: beforePhotos.value.length,
        afterCount: afterPhotos.value.length,
        beforeImg,
        afterImg,
        testimonial: testimonial || ''
      })
    } catch (drawErr) {
      console.error(drawErr)
      uni.showToast({ title: '绘制失败，请重试', icon: 'none' })
      return
    }

    const tempPath = await new Promise((resolve, reject) => {
      const opts = {
        canvas,
        fileType: 'png',
        quality: 1,
        destWidth: POSTER_W,
        destHeight: posterH,
        success: (out) => resolve(out.tempFilePath),
        fail: (e) => reject(e)
      }
      if (proxy) uni.canvasToTempFilePath(opts, proxy)
      else uni.canvasToTempFilePath(opts)
    })

    await new Promise((resolve) => {
      uni.saveImageToPhotosAlbum({
        filePath: tempPath,
        success: () => {
          uni.showToast({ title: '已保存到相册', icon: 'success' })
          resolve()
        },
        fail: (err) => {
          const msg = err.errMsg || ''
          if (msg.includes('auth deny') || msg.includes('authorize') || msg.includes('auth')) {
            uni.showModal({
              title: '需要相册权限',
              content: '保存海报需要授权保存到相册，请在设置中开启。',
              confirmText: '去设置',
              success: (m) => { if (m.confirm) uni.openSetting({}) }
            })
          } else {
            uni.showToast({ title: msg.length < 60 ? msg : '保存失败', icon: 'none' })
          }
          resolve()
        }
      })
    })
  } catch (e) {
    const em = e && e.errMsg ? String(e.errMsg) : ''
    if (em.includes('canvasToTempFilePath')) {
      uni.showToast({ title: '导出图片失败，请重试', icon: 'none' })
    } else {
      uni.showToast({ title: '生成失败', icon: 'none' })
    }
  } finally {
    uni.hideLoading()
  }
}
// #endif

onLoad((options) => {
  routeToken.value = options?.token || ''
})
onMounted(() => loadReport())
onUnmounted(() => stopHighlightPoll())
</script>

<style scoped>
.report-view {
  background: #fafaf8;
  min-height: 100vh;
}
.poster-offscreen {
  position: fixed;
  left: -9999px;
  top: 0;
  width: 375px;
  height: 610px;
  pointer-events: none;
}

/* 加载/空状态 */
.state-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  gap: 12px;
}
.state-spinner {
  width: 28px; height: 28px;
  border: 2.5px solid #ebebea;
  border-top-color: #2db96d;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.state-text { font-size: 14px; color: #999993; }
.state-emoji { font-size: 36px; }

/* 内容 */
.report-content {
  max-width: 430px;
  margin: 0 auto;
  background: #fff;
  min-height: 100vh;
}

/* 品牌栏 */
.brand-bar {
  background: #2db96d;
  padding: 12px 16px 14px;
  border-bottom-left-radius: 14px;
  border-bottom-right-radius: 14px;
}
.brand-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.brand-home {
  width: 32px; height: 32px;
  border-radius: 10px;
  background: rgba(255,255,255,0.12);
  display: flex;
  align-items: center;
  justify-content: center;
}
.brand-name {
  font-size: 16px;
  font-weight: 700;
  color: #fff;
  letter-spacing: 0.5px;
}

/* 主视觉：照片对比 */
.hero-stage {
  position: relative;
  margin: 12px 16px 0;
  border-radius: 18px;
  overflow: hidden;
  background: #f5f5f2;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.hero-photos {
  display: flex;
  height: 280px;
  overflow: hidden;
}
.hero-before, .hero-after {
  flex: 1;
  position: relative;
  overflow: hidden;
}
.hero-before { border-right: 1.5px solid #fff; }
.hero-after { border-left: 1.5px solid #fff; }
.hero-img {
  width: 100%;
  height: 100%;
  display: block;
}
.hero-empty {
  width: 100%;
  height: 100%;
  background: #f0f0ee;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-empty-text {
  font-size: 13px;
  color: #bbbbb5;
}
.hero-seq {
  position: absolute;
  top: 10px;
  right: 10px;
  min-width: 44px;
  height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  z-index: 2;
}
.hero-seq-before {
  background: rgba(245, 145, 62, 0.9);
  color: #fff;
}
.hero-seq-after {
  background: rgba(45, 185, 109, 0.9);
  color: #fff;
}
.hero-tag {
  position: absolute;
  bottom: 10px;
  left: 10px;
  padding: 3px 10px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
.tag-before {
  background: rgba(245, 145, 62, 0.9);
  color: #fff;
}
.tag-after {
  background: rgba(45, 185, 109, 0.9);
  color: #fff;
}
.hero-divider {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  pointer-events: none;
}
.hero-divider-text {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 58px;
  height: 28px;
  padding: 0 12px;
  border-radius: 999px;
  background: rgba(26, 26, 26, 0.82);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
}

/* 照片信息与补充分组 */
.hero-meta {
  display: flex;
  gap: 8px;
  padding: 10px 16px 0;
}
.hero-tip {
  padding: 8px 16px 0;
  text-align: center;
}
.hero-tip text {
  font-size: 12px;
  color: #bbbbb5;
  letter-spacing: 0.2px;
}
.hero-actions {
  display: flex;
  gap: 8px;
  padding: 10px 16px 0;
}
.hero-action-chip {
  flex: 1;
  min-width: 0;
  height: 40px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.2px;
}
.hero-action-chip-before {
  background: #fff7f0;
  color: #f5913e;
  border: 1px solid rgba(245, 145, 62, 0.18);
}
.hero-action-chip-after {
  background: #f3fbf6;
  color: #2db96d;
  border: 1px solid rgba(45, 185, 109, 0.18);
}
.hero-meta-chip {
  flex: 1;
  min-width: 0;
  height: 36px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}
.hero-meta-chip-before {
  background: #fef5ec;
  color: #f5913e;
}
.hero-meta-chip-after {
  background: #eaf8f0;
  color: #2db96d;
}
.photo-groups {
  padding: 12px 16px 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.photo-group-card {
  background: #fafaf8;
  border-radius: 14px;
  padding: 14px;
}
.photo-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.photo-group-title {
  font-size: 14px;
  font-weight: 700;
  color: #1a1a1a;
}
.photo-group-count {
  font-size: 12px;
  color: #999993;
}
.photo-grid-extra {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.photo-grid-extra-item {
  position: relative;
}
.photo-grid-extra-img {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  background: #f0f0ee;
  display: block;
}
.photo-grid-extra-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: rgba(245, 145, 62, 0.92);
  color: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
}
.photo-grid-extra-badge-after {
  background: rgba(45, 185, 109, 0.92);
}

/* 问候 */
.greeting-card {
  margin: 16px 16px 0;
  padding: 14px 16px;
  background: #fef9f0;
  border-radius: 12px;
}
.greeting-text {
  font-size: 14px;
  color: #666660;
  line-height: 1.6;
}
.greeting-pet {
  font-weight: 700;
  color: #1a1a1a;
}

/* 信息卡 */
.info-card {
  margin: 16px;
  padding: 20px;
  background: #fafaf8;
  border-radius: 14px;
}
.info-title {
  font-size: 13px;
  font-weight: 700;
  color: #999993;
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: 14px;
}
.info-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
}
.info-row + .info-row {
  border-top: 1px solid #f0f0ee;
}
.info-key {
  font-size: 14px;
  color: #999993;
}
.info-val {
  font-size: 14px;
  font-weight: 600;
  color: #1a1a1a;
}

/* 备注 */
.remark-card {
  margin: 0 16px 16px;
  padding: 16px;
  background: #fafaf8;
  border-radius: 14px;
}
.remark-label {
  display: block;
  font-size: 13px;
  font-weight: 700;
  color: #999993;
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: 8px;
}
.remark-text {
  font-size: 14px;
  color: #666660;
  line-height: 1.6;
}

/* 服务回顾短片 */
.highlight-card {
  margin: 0 16px 16px;
  padding: 16px;
  background: linear-gradient(145deg, #f3fff9 0%, #eef8ff 100%);
  border-radius: 14px;
  border: 1px solid #dcefe3;
}
.highlight-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.highlight-title {
  font-size: 16px;
  font-weight: 700;
  color: #14532d;
}
.highlight-badge {
  font-size: 11px;
  font-weight: 700;
  color: #0f766e;
  background: rgba(45, 185, 109, 0.15);
  padding: 3px 10px;
  border-radius: 999px;
}
.highlight-desc {
  display: block;
  font-size: 12px;
  color: #4b5563;
  line-height: 1.55;
  margin-bottom: 12px;
}
.highlight-status {
  padding: 10px 12px;
  border-radius: 10px;
  font-size: 13px;
  margin-bottom: 10px;
}
.highlight-status--busy {
  background: rgba(45, 185, 109, 0.12);
  color: #166534;
}
.highlight-status--err {
  background: rgba(239, 68, 68, 0.1);
  color: #b91c1c;
}
.highlight-player {
  width: 100%;
  max-height: 420px;
  border-radius: 12px;
  background: #000;
  margin-bottom: 12px;
}
.highlight-actions {
  display: flex;
  gap: 10px;
}
.highlight-btn {
  flex: 1;
  font-size: 14px;
  font-weight: 600;
  padding: 10px 8px;
  border-radius: 10px;
  border: none;
  background: #2db96d;
  color: #fff;
}
.highlight-btn--secondary {
  background: #0d9488;
}
.highlight-btn[disabled] {
  opacity: 0.55;
}

/* 操作按钮 */
.actions {
  padding: 0 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.action-link-btn {
  height: 42px !important;
  min-height: 42px !important;
  line-height: 42px !important;
  font-weight: 600 !important;
}

/* 底部品牌 */
.footer-brand {
  padding: 24px 16px 40px;
  text-align: center;
}
.footer-line {
  display: block;
  width: 24px;
  height: 2px;
  background: #ebebea;
  margin: 0 auto 12px;
  border-radius: 1px;
}
.footer-name {
  display: block;
  font-size: 14px;
  font-weight: 700;
  color: #1a1a1a;
  letter-spacing: 0.5px;
}
.footer-sub {
  display: block;
  font-size: 11px;
  color: #bbbbb5;
  margin-top: 4px;
  letter-spacing: 0.5px;
}

/* 过程中区块 */
.section-gap {
  padding: 0 16px;
}
.during-section {
  background: #fff;
  border-radius: 16px;
  padding: 18px 18px 4px;
}
.during-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 700;
  color: #1a1a1a;
  margin-bottom: 14px;
}
.during-title-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #7c5ce9;
}
.during-group {
  margin-bottom: 14px;
}
.during-group-label {
  font-size: 12px;
  font-weight: 600;
  color: #999993;
  margin-bottom: 8px;
}
.during-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.during-thumb {
  position: relative;
  aspect-ratio: 1;
  border-radius: 10px;
  overflow: hidden;
  background: #f0f0ee;
}
.during-thumb-img {
  width: 100%;
  height: 100%;
  display: block;
}
</style>
