<template>
  <div class="article-reading-preview">
    <!-- resume 探针在途：隐藏起始页，避免刷新闪开始页 -->
    <div v-if="resuming" class="article-report-loading">
      <span class="spinner"></span>
    </div>

    <!-- ready -->
    <div v-else-if="engine.stage.value === 'ready'" class="article-ready-stage">
      <h1 class="ready-title">{{ articleStore.config.title || lang.ssArticleReading }}</h1>
      <p v-if="hasArticle" class="ready-summary">{{ articleSummary }}</p>
      <p v-else class="ready-placeholder">{{ lang.ssArticlePleaseConfigure }}</p>

      <p v-if="hasArticle" class="ready-meta">
        {{ articleStore.config.practice.mode === 'time'
          ? `${articleStore.config.practice.duration} ${lang.ssMinute}`
          : lang.ssArticleUnlimitedLabel }}
      </p>

      <p v-if="readyError" class="article-error">{{ readyError }}</p>

      <button class="start-btn" :disabled="startDisabled" @click="engine.startSession">
        {{ lang.ssArticleStart }}
      </button>
    </div>

    <!-- reading -->
    <ArticleReadingView
      v-else-if="engine.stage.value === 'reading' && engine.report.value"
      :paragraphs="engine.report.value.paragraphs"
      :current-paragraph-idx="engine.currentParagraphIdx.value"
      :recording-state="engine.recordingState.value"
      :recording-duration="engine.recordingDuration.value"
      :elapsed-practice-seconds="engine.elapsedPracticeSeconds.value"
      :show-report="showReport"
      :session-ended="engine.sessionEnded.value"
      :overall-started="engine.overallStarted.value"
      :can-view-report="engine.canViewReport.value"
      :local-audio-idxs="localAudioIdxs"
      :playing-id="engine.playingId.value"
      :loading-id="engine.loadingId.value"
      :error="readingError"
      @select-paragraph="engine.selectParagraph"
      @start-recording="engine.startRecording"
      @finish-recording="engine.finishRecording"
      @cancel-recording="engine.cancelRecording"
      @play-demo="engine.playDemoAudio"
      @play-own="engine.playOwnAudio"
      @view-report="engine.completeAndShowReport"
      @restart="handleRestart"
      @dismiss-error="engine.dismissError"
    />

    <!-- report-loading：一次性显示门，不暴露半成品报告 -->
    <div v-else-if="engine.stage.value === 'report-loading'" class="article-report-loading">
      <span class="spinner"></span>
      <p class="loading-text">{{ lang.ssArticleGeneratingReport }}</p>
      <p v-if="engine.error.value" class="article-error">{{ engine.error.value }}</p>
      <button v-if="engine.error.value" class="retry-btn" @click="engine.completeAndShowReport">
        {{ lang.ssArticleRetry }}
      </button>
    </div>

    <!-- report -->
    <ArticleDetailedReport
      v-else-if="engine.report.value"
      :report="engine.report.value"
      :playing-id="engine.playingId.value"
      :loading-id="engine.loadingId.value"
      show-restart
      @play-demo="engine.playDemoAudio"
      @play-own="engine.playOwnAudio"
      @restart="handleRestart"
    />
  </div>
</template>

<script lang="ts" setup>
import { ref, computed, watch, toRef, inject, onUnmounted } from 'vue'
import { lang } from '@/main'
import ArticleReadingView from './ArticleReadingView.vue'
import ArticleDetailedReport from './ArticleDetailedReport.vue'
import { useArticleReadingEngine } from '../composables/useArticleReadingEngine'
import { useArticleReadingStore } from '@/store/articleReading'
import { getSpeakingConfig } from '@/services/speaking'
import { getLatestArticleSession } from '../services/articleReading'
import { friendlyArticleError } from '../services/articleErrors'
import { ARTICLE_READING_TOOL_TYPE } from '@/configs/englishSpeakingTools'
import message from '@/utils/message'
import type { ArticleReadingConfig } from '@/types/articleReading'

const props = defineProps<{
  configId: string
}>()

const articleStore = useArticleReadingStore()
const loadError = ref<string | null>(null)

// 'active' 覆盖两种「这一轮尚未完成」：真正开始，以及重新开始把旧轮作废。
// 老师端忽略这个值、只是据此重取，所以关键在于广播有没有发出去。
type SpeakingNotify = (
  status: 'active' | 'completed',
  payload: { configId: string; sessionId: string },
) => void
const notifySpeakingProgress = inject<SpeakingNotify>('notifySpeakingProgress', () => {})
const recordSpeakingStart = inject<(sessionId: string, toolType?: number) => void>(
  'recordSpeakingStart',
  () => {},
)

// **两道门缺一不可**：`type` 说这个人是什么身份，`userid` 才说他是谁。
// 只看 userid 会把老师也算进去 —— 编辑器 URL（App.vue:103）自己就带着 userid。
//
// 身份用项目既有的 `type`（`Student/index.vue:625`：老师端 type=1、学生端 type=2），
// 不用 `mode`：`mode=student` 是**视图**不是身份，老师上课时走的也是这个视图
// （班级面板就渲染在里面）。拿视图当身份，老师在上课页试录就会被算成学生 —— 正是
// 本次要修的那个 bug 换个地方复发。`type` 是身份，与哪个视图被渲染出来无关。
const runtimeParams = computed(() => {
  const params = new URLSearchParams(window.location.search)
  return { role: params.get('type'), userId: params.get('userid') || '' }
})
const isStudentRuntime = computed(() => runtimeParams.value.role === '2')
const runtimeUserId = computed(() => runtimeParams.value.userId)
/** 传给后端的身份：编辑态是 null = 老师预览（spec §4.5），不进任何统计。 */
const sessionUserId = computed(() => (isStudentRuntime.value ? runtimeUserId.value : null))

/**
 * 缺省为 true。store 用 Object.assign 合并服务端配置，所以一行 `evaluation: {}` 会把
 * 默认值冲掉、留下 undefined —— 而 undefined 落到的是最严的那一档（一次机会、永久只读、
 * 无报告、老师端也没有重置入口）。现有写入方都带这个键，但失败方向不对，值一行钱。
 */
const showReport = computed(() => articleStore.config.evaluation.showReport !== false)

const engine = useArticleReadingEngine({
  configId: toRef(props, 'configId'),
  userId: sessionUserId,
  durationMinutes: computed(() => articleStore.config.practice.duration),
  showReport,
  onSessionStarted(sessionId) {
    if (!isStudentRuntime.value) return
    notifySpeakingProgress('active', { configId: props.configId, sessionId })
    recordSpeakingStart(sessionId, ARTICLE_READING_TOOL_TYPE)
  },
  onSessionCompleted(sessionId) {
    if (!isStudentRuntime.value) return
    notifySpeakingProgress('completed', { configId: props.configId, sessionId })
  },
  onSessionAbandoned(sessionId) {
    if (!isStudentRuntime.value) return
    // 库里这一轮已经不作数了，但老师端只在收到学生广播时重取。不发这一条，他会一直
    // 看着一个「已完成 + 分数」，点进去还是作废那轮的报告（spec §4.2）。
    notifySpeakingProgress('active', { configId: props.configId, sessionId })
  },
})

const localAudioIdxs = computed(() => [...engine.localAudioByParagraph.value.keys()])

/**
 * 练习页要显示的错误文案。后端 409 = 这一轮已被作废，重试没有意义，
 * 直接换成「请重新开始」，而不是把 `{"detail":"..."}` 原样甩给学生。
 */
const readingError = computed(() => {
  if (engine.sessionInvalid.value) return lang.ssArticleSessionExpired as string
  return engine.error.value
})

async function handleRestart() {
  const ok = await engine.restartSession()
  if (!ok) {
    message.error(lang.ssArticleRestartFailed as string)
    return
  }
  // 与另外两处 reset() 调用点一致：作废在途的 resume 探针，否则它落地时会把刚清掉的
  // 那一轮又水合回来。今天走不到（探针只在 resuming 遮住 UI 时跑），但这是唯一一条
  // 不作废探针的 reset 路径，留着就是下一个人的坑。
  resumeToken += 1
}

const hasArticle = computed(() =>
  Boolean(articleStore.config.title.trim() && articleStore.config.content.trim()),
)

const articleSummary = computed(() => {
  const content = articleStore.config.content.trim()
  return content.length > 120 ? `${content.slice(0, 120)}...` : content
})

/** 还没开始就已知的阻塞原因：配置没加载出来、或学生态缺 userid。这两条也决定按钮是否可点。 */
const startBlockedReason = computed(() => {
  if (loadError.value) return loadError.value
  // 编辑态没有身份是正常的（那就是老师预览）；只有学生态缺 userid 才是真的进不去。
  if (hasArticle.value && isStudentRuntime.value && !runtimeUserId.value) {
    return lang.ssArticleMissingUser as string
  }
  return null
})

/**
 * ready 页面真正渲染的那句话（spec §9.2.5 A2）。
 * 除了「按不下去的原因」，还必须包含「按下去了但失败了」—— 此前 engine.error 在这个 stage
 * 根本没有出口，`POST /session` 一失败学生看到的就是一片空白：按钮弹回、什么都没说。
 */
const readyError = computed(() => startBlockedReason.value || engine.error.value)

// 只看阻塞原因，不看 engine.error：开始失败之后学生应该还能再点一次。
// `engine.starting` 是例外 —— POST 在途时必须置灰，否则双击会建出两条 session
// （后端 get-or-create 无锁，见 useArticleReadingEngine.startSession 的注释）。
const startDisabled = computed(
  () => !hasArticle.value || engine.starting.value || Boolean(startBlockedReason.value),
)

async function loadConfig() {
  loadError.value = null
  if (!props.configId) return
  try {
    const record = await getSpeakingConfig<ArticleReadingConfig>(props.configId)
    if (record.config.type !== 'article') {
      throw new Error('Speaking config is not an article')
    }
    articleStore.replaceConfig(record.config)
  } catch (cause) {
    console.error('[article-reading] load config failed:', cause)
    // 共用端点的 parse() 抛的是 `[404] {"detail":"..."}`，直接渲染就是把原始 JSON
    // 甩给用户。走同一层：命中就是中文，命不中也是兜底文案，不会是响应体。
    loadError.value = friendlyArticleError(cause)
  }
}

// resume 探针在途标记：为 true 时隐藏 ready 起始页，避免刷新瞬间闪一下开始页
// （对齐话题讨论的 checking-history）。仅学生端（有 userid）才探测。
const resuming = ref(false)
// 作废在途 resume：configId 变化 / 老师重置时递增，丢弃过期探针结果
let resumeToken = 0

async function maybeResumeSession(token: number) {
  // 编辑态不探测：预览 session 的 user_id 是 NULL，后端按定义查不到（spec §4.5），
  // 而 GET /sessions/latest 对空 userId 仍然回 400 —— 发出去只会在控制台留一条噪音。
  if (!isStudentRuntime.value) return
  if (!props.configId || !runtimeUserId.value) return
  try {
    const { session } = await getLatestArticleSession(props.configId, runtimeUserId.value)
    if (token !== resumeToken || !session) return
    // 有历史 session → 跳过起始页，直接回到朗读 / 结果页
    engine.resumeFromReport(session)
    notifySpeakingProgress(
      session.status === 'completed' ? 'completed' : 'active',
      { configId: props.configId, sessionId: session.sessionId },
    )
  }
  catch (cause) {
    console.error('[article-reading] resume probe failed:', cause)
    // 探针失败 → 保持 ready 起始页，学生可手动开始
  }
  finally {
    if (token === resumeToken) resuming.value = false
  }
}

watch(() => props.configId, async () => {
  const token = ++resumeToken
  engine.reset()
  resuming.value = Boolean(props.configId && isStudentRuntime.value && runtimeUserId.value)
  await loadConfig()
  if (token !== resumeToken) return
  await maybeResumeSession(token)
}, { immediate: true })

// 把阶段同步进 store，供 CanvasTool 决定「重置预览」显不显示（镜像话题讨论
// TopicDiscussionPreview 的 setPreviewState）。`immediate` 不能省：切页换实例时新预览
// 一挂就得把 store 拨回 ready，否则按钮沿用上一个实例的阶段。
watch(() => engine.stage.value, (stage) => {
  articleStore.setPreviewStage(stage)
}, { immediate: true })

// 老师在配置面板点应用 → 预览回到 ready 并重载（编辑态不 resume，见 maybeResumeSession）
watch(() => articleStore.resetSignal, () => {
  resumeToken += 1
  resuming.value = false
  engine.reset()
  void loadConfig()
})

onUnmounted(() => engine.dispose())
</script>

<style lang="scss" scoped>
.article-reading-preview {
  position: relative;
  width: 100%;
  height: 100%;
  background: #fff;
  overflow: hidden;
}

// ---- ready ----
.article-ready-stage {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 32px;
  text-align: center;
}

.ready-title {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  color: #111827;
}

.ready-summary {
  margin: 0;
  max-width: 560px;
  font-size: 14px;
  line-height: 1.7;
  color: #6b7280;
}

.ready-placeholder {
  margin: 0;
  font-size: 14px;
  color: #9ca3af;
}

.ready-meta {
  margin: 0;
  font-size: 12px;
  color: #9ca3af;
}

.start-btn {
  margin-top: 8px;
  padding: 10px 32px;
  border: none;
  border-radius: 999px;
  background: #f97316;
  color: #fff;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  box-shadow: 0 4px 6px -1px rgba(249, 115, 22, 0.25);
  transition: background 0.2s;

  &:hover:not(:disabled) { background: #ea580c; }

  &:disabled {
    background: #d1d5db;
    box-shadow: none;
    cursor: not-allowed;
  }
}

// ---- report loading ----
.article-report-loading {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 32px;
  text-align: center;
}

.spinner {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid #fb923c;
  border-top-color: transparent;
  animation: article-preview-spin 0.8s linear infinite;
}

@keyframes article-preview-spin {
  to { transform: rotate(360deg); }
}

.loading-text {
  margin: 0;
  font-size: 14px;
  color: #6b7280;
}

.article-error {
  margin: 0;
  font-size: 12px;
  color: #dc2626;
}

.retry-btn {
  padding: 6px 20px;
  border: 1px solid #fdba74;
  border-radius: 999px;
  background: #fff;
  color: #ea580c;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;

  &:hover { background: #fff7ed; }
}
</style>
