<template>
  <div class="article-practice-config">
    <!-- 滚动内容区域 -->
    <div class="config-scroll">

      <div class="config-card">

        <!-- 1. 文章标题 -->
        <div class="form-group">
          <label class="form-label">{{ lang.ssArticleTitle }}</label>
          <input
            type="text"
            :value="store.config.title"
            @input="store.config.title = ($event.target as HTMLInputElement).value"
            class="form-input"
            :placeholder="lang.ssArticleTitlePlaceholder as string"
          />
        </div>

        <!-- 2. 文章正文 -->
        <div class="form-group">
          <div class="label-row">
            <label class="form-label">{{ lang.ssArticleContent }}</label>
            <button
              class="btn-auto-format"
              :disabled="!store.config.content.trim() || isFormatting"
              @click="openOverwrite"
            >
              {{ lang.ssArticleAutoFormat }}
            </button>
          </div>
          <textarea
            ref="contentRef"
            :value="store.config.content"
            :disabled="isFormatting"
            @input="onContentInput(($event.target as HTMLTextAreaElement).value)"
            class="form-textarea"
            :placeholder="lang.ssArticleContentPlaceholder as string"
          ></textarea>
        </div>

        <!-- 3. 练习时长 -->
        <div class="form-group slider-group">
          <label class="sub-label">{{ lang.ssArticleTotalDuration }}</label>
          <input
            type="range"
            :value="store.config.practice.duration"
            min="1" max="60" step="1"
            class="config-slider"
            @input="store.config.practice.duration = Number(($event.target as HTMLInputElement).value)"
          />
          <div class="slider-labels">
            <span class="slider-min">1 {{ lang.ssMinute }}</span>
            <span class="slider-value">{{ store.config.practice.duration }} {{ lang.ssMinute }}</span>
            <span class="slider-max">60 {{ lang.ssMinute }}</span>
          </div>
        </div>

        <!-- 4. 展示学习报告 -->
        <div class="toggle-row">
          <span class="toggle-label">{{ lang.ssShowReport }}</span>
          <button
            class="toggle-switch"
            :class="{ on: store.config.evaluation.showReport }"
            @click="store.config.evaluation.showReport = !store.config.evaluation.showReport"
          >
            <span class="toggle-knob"></span>
          </button>
        </div>

      </div>

      <div class="bottom-spacer"></div>
    </div>

    <!-- 底部操作栏 —— 不置灰：有人在练也照常可按，后端 409 时弹确认框（spec §4.3 B） -->
    <div class="config-footer">
      <button
        class="btn-apply"
        :disabled="applying || isFormatting"
        @click="handleApply"
      >
        <!-- 保存会同步把整篇示范音频合成好，正文改过的那次要几秒。按钮必须说出
             它在等什么 —— 只转圈或只置灰，老师看到的是「应用配置卡住了」。 -->
        <span v-if="applying" class="btn-apply__spinner"></span>
        {{ applying ? lang.ssArticleGeneratingDemo : lang.ssApplyConfig }}
      </button>
    </div>

    <ArticleFormatDialog
      :visible="dialogVisible"
      :mode="dialogMode"
      :loading="isFormatting"
      @confirm="confirmDialog"
      @cancel="handleDialogCancel"
    />
  </div>
</template>

<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue'
import { lang } from '@/main'
import { useArticleReadingStore } from '@/store/articleReading'
import useCreateElement from '@/hooks/useCreateElement'
import { getSpeakingConfig } from '@/services/speaking'
import { createArticleConfig, updateArticleConfig } from '../services/articleReading'
import { articleActiveCountFromError, friendlyArticleError } from '../services/articleErrors'
import { ARTICLE_READING_TOOL_TYPE } from '@/configs/englishSpeakingTools'
import type { ArticleReadingConfig } from '@/types/articleReading'
import message from '@/utils/message'
import { showConfirmDialog } from '@/utils/confirmDialog'
import ArticleFormatDialog from '../components/ArticleFormatDialog.vue'
import { useArticleAutoFormat } from '../composables/useArticleAutoFormat'

defineEmits<{
  (e: 'back'): void
}>()

const store = useArticleReadingStore()
const { createFrameElement } = useCreateElement()
const applying = ref(false)

// 自动分段（spec §3.2）：三种触发共用一个弹框状态机
const contentRef = ref<HTMLTextAreaElement | null>(null)
const content = computed({
  get: () => store.config.content,
  set: (value: string) => {
    store.config.content = value
  },
})
const {
  dialogVisible,
  dialogMode,
  isFormatting,
  onContentInput,
  openOverwrite,
  guardBeforeApply,
  confirmDialog,
  cancelDialog,
} = useArticleAutoFormat(content)

// 守卫框的「手动修改」= 关框 + 焦点回文本框；其余 mode 只关框
const handleDialogCancel = () => {
  if (isFormatting.value) return
  const wasGuard = dialogMode.value === 'guard'
  cancelDialog()
  if (wasGuard) nextTick(() => contentRef.value?.focus())
}

// 载入已有配置：竞态用 token 作废旧响应，防慢请求覆盖新选中的元素
let loadToken = 0
watch(() => store.activeConfigId, async (configId) => {
  if (!configId) return
  const token = ++loadToken
  try {
    const { config } = await getSpeakingConfig<ArticleReadingConfig>(configId)
    if (token !== loadToken) return
    if (config?.type !== 'article') {
      console.warn('[article-reading] ignored non-article config:', configId)
      return
    }
    store.replaceConfig(config)
  } catch (err) {
    if (token !== loadToken) return
    console.error('[article-reading] load config failed:', err)
  }
}, { immediate: true })

/**
 * 保存到已有 config。返回 **false = 老师在确认框里选了取消**，一个字都没写。
 *
 * 409 是问句不是墙（spec §4.3 B，2026-07-24 定）：detail 里带着按下那一刻的在练人数，
 * 用它填确认框把后果说清楚 —— 那 N 个学生刷新会回到开始页、这一轮成绩不进班级名单 ——
 * 老师选「仍要保存」就带 force 原样重发一次。绝不把 409 的原始 JSON 甩给老师
 * （承 §9.2.5 那次教训：`request()` 抛的是 `response.text()`）。
 */
async function saveExistingConfig(configId: string): Promise<boolean> {
  try {
    await updateArticleConfig(configId, store.config)
    return true
  }
  catch (err: unknown) {
    const practising = articleActiveCountFromError(err)
    if (practising === null) throw err // 别的错误照旧交给 handleApply 的 catch

    const confirmed = await showConfirmDialog({
      title: lang.ssArticlePractisingOverwriteTitle as string,
      content: String(lang.ssArticlePractisingOverwriteMessage).replace('{n}', String(practising)),
      confirmText: lang.ssArticlePractisingOverwriteConfirm as string,
      cancelText: lang.ssArticlePractisingOverwriteCancel as string,
    })
    if (!confirmed) return false

    await updateArticleConfig(configId, store.config, true)
    return true
  }
}

const handleApply = async () => {
  if (applying.value) return
  if (!store.config.title.trim() || !store.config.content.trim()) {
    message.error(lang.ssArticleRequired as string)
    return
  }
  // 换行归一化回写 + 单段 300 词硬守卫（spec §3.2 step 4）；被拦下时弹框已开
  if (!guardBeforeApply()) return

  applying.value = true
  try {
    // 写入目标 = **面板正在编辑的那个 config**。`activeConfigId` 由两个入口置上：双击画布上的
    // 文章朗读元素（`FrameElement`）、Layer2 插入新工具。面板的表单内容也是照它加载的
    // （见下方 watch），所以它才是唯一正确的写入目标。
    //
    // 曾经的写法是「在当前幻灯片上 find 第一个 article frame」，那是错的（2026-07-27 实测撞到）：
    // 面板开着切页后，find 命中的是**另一个**元素 —— 把 A 的正文写进 B；一个都找不到时更糟，
    // 掉进下面的新建分支凭空多一个 config，老师看到「保存成功」，而学生手上那个 config 一个字
    // 都没变，服务端的在练检查也就只数了那个新 config 名下的 0 个人，怎么看都「拦不住」。
    if (store.activeConfigId) {
      // 文章专属的保存接口（spec §4.3 B）：服务端在真正写入的那一刻再数一次在练人数 ——
      // 面板不预先问、也不置灰，挡不住的正是「老师想了两分钟、期间有学生按了开始」这种时间差。
      // 取消 = 什么都没发生：不重置预览、不弹「已保存」。
      if (!await saveExistingConfig(store.activeConfigId)) return
    } else {
      // 面板没有可编辑的 config（两个入口都会带 id，正常进不到这里）。当作全新建。
      // 走文章自己的 POST 而不是共用的那个：新建同样要把整篇示范音频合成好才写入，
      // 否则这条兜底路生出来的 config 是唯一一份「学生点示范要等」的配置。
      const { id } = await createArticleConfig(store.config)
      createFrameElement(id, ARTICLE_READING_TOOL_TYPE)
      store.setActiveConfigId(id)
    }
    store.requestResetPreview()
    message.success(lang.ssArticleSaved as string)
  } catch (err: unknown) {
    console.error('[article-reading] apply failed:', err)
    // 「有人在练」那一条已经在 saveExistingConfig 里变成确认框了，走到这里的都是真错误
    message.error(friendlyArticleError(err))
  } finally {
    applying.value = false
  }
}
</script>

<style lang="scss" scoped>
.article-practice-config {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.config-scroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.config-card {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 16px;
}

.form-group {
  margin-bottom: 12px;

  &:last-child {
    margin-bottom: 0;
  }
}

.form-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: #374151;
  margin-bottom: 6px;
}

.label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;

  .form-label {
    margin-bottom: 0;
  }
}

/* enspeak: px-2.5 py-1 text-[11px] text-orange-600 bg-orange-50 hover:bg-orange-100
   disabled:bg-gray-100 disabled:text-gray-400 rounded-md font-medium */
.btn-auto-format {
  padding: 4px 10px;
  border: none;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 500;
  color: #ea580c;
  background: #fff7ed;
  cursor: pointer;
  transition: background 0.2s;

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

  &:disabled {
    background: #f3f4f6;
    color: #9ca3af;
    cursor: not-allowed;
  }
}

.sub-label {
  display: block;
  font-size: 12px;
  color: #6b7280;
  margin-bottom: 6px;
}

.form-input {
  width: 100%;
  height: 36px;
  padding: 0 12px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  font-size: 13px;
  color: #374151;
  background: #fff;
  transition: all 0.2s;
  box-sizing: border-box;

  &:focus {
    outline: none;
    border-color: #f97316;
  }

  &::placeholder {
    color: #9ca3af;
  }
}

.form-textarea {
  width: 100%;
  height: 128px;
  padding: 8px 12px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: #374151;
  background: #fff;
  font-family: inherit;
  resize: none;
  transition: all 0.2s;
  box-sizing: border-box;

  &:focus {
    outline: none;
    border-color: #f97316;
  }

  &::placeholder {
    color: #9ca3af;
  }

  &:disabled {
    background: #f3f4f6;
    color: #9ca3af;
    cursor: not-allowed;
  }
}

.config-slider {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  appearance: none;
  background: #e5e7eb;
  cursor: pointer;
  accent-color: #f97316;

  &::-webkit-slider-thumb {
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #f97316;
    cursor: pointer;
    border: 2px solid #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  }
}

.slider-labels {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 6px;
  font-size: 12px;
}

.slider-min, .slider-max {
  color: #9ca3af;
}

.slider-value {
  font-weight: 500;
  color: #374151;
}

.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 12px;
}

.toggle-label {
  font-size: 12px;
  color: #4b5563;
}

.toggle-switch {
  position: relative;
  width: 44px;
  height: 24px;
  border-radius: 12px;
  border: none;
  cursor: pointer;
  transition: background 0.2s;
  background: #d1d5db;

  &.on {
    background: #f97316;

    .toggle-knob {
      left: 22px;
    }
  }
}

.toggle-knob {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  transition: left 0.2s;
}

.bottom-spacer {
  height: 24px;
}

.config-footer {
  border-top: 1px solid #f3f4f6;
  padding: 12px 16px;
  background: #fff;
  flex-shrink: 0;
}

.btn-apply {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 36px;
  border: none;
  border-radius: 8px;
  background: #f97316;
  color: #fff;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;

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

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

.btn-apply__spinner {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  animation: article-apply-spin 0.7s linear infinite;
}

@keyframes article-apply-spin {
  to { transform: rotate(360deg); }
}
</style>
