<script setup lang="ts">
import {
  DeleteOutlined,
  HolderOutlined,
  LeftOutlined,
  PlusOutlined,
  RobotOutlined,
  StarFilled,
} from '@ant-design/icons-vue'
import type {
  ArticleDetail,
  JsonValue,
  Question,
  TemplateAnalyzeResult,
  TemplateAssistCompetitor,
  TemplateOutlineNode,
} from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'

import AiWaitingModal from '@/components/AiWaitingModal.vue'
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
import { articlesApi, brandsApi, normalizeInputParams, templatesApi } from '@/lib/api'
import { getTemplateMeta } from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'

interface DraftCompetitor {
  key: string
  libraryId?: number
  name: string
  website: string
  description: string
  saved: boolean
}

interface OutlineDraftNode {
  id: string
  outline: string
  children: OutlineDraftNode[]
}

interface TemplateHeroConfig {
  accent?: string
  helper?: string
}

interface TemplateOutlineSectionConfig {
  key: string
  label?: string
  label_template?: string
  default?: boolean
}

interface TemplateOutlineConfig {
  allow_custom?: boolean
  custom_placeholder?: string
  custom_max_length?: number
  preview_title?: string
  preview_caption?: string
  sections?: TemplateOutlineSectionConfig[]
}

interface TemplateStepConfig {
  title?: string
  description?: string
}

interface TemplateCardCopy {
  title?: string
  hint?: string
  visible?: boolean
}

interface TemplateReviewIntroHookOption {
  key: string
  label: string
  description?: string
  default?: boolean
}

interface TemplateReviewIntroHookConfig {
  title?: string
  hint?: string
  field_name?: string
  required?: boolean
  options?: TemplateReviewIntroHookOption[]
}

interface TemplateBasicConfig {
  analyze_button_label?: string
  cards?: {
    brand?: TemplateCardCopy
    keywords?: TemplateCardCopy
    template_fields?: TemplateCardCopy
    competitors?: TemplateCardCopy
  }
}

interface TemplateStructureConfig {
  cards?: {
    choose_title?: TemplateCardCopy
    outline?: TemplateCardCopy
    preview?: TemplateCardCopy
  }
  review_intro_hook?: TemplateReviewIntroHookConfig
}

interface TemplateReviewConfig {
  card?: TemplateCardCopy
  alert_message?: string
}

interface TemplateDerivedInputConfig {
  source?: string
  fallback_number?: number
}

interface TemplateWizardConfig {
  managed_fields?: string[]
  derived_inputs?: Record<string, TemplateDerivedInputConfig>
  steps?: {
    basic?: TemplateStepConfig
    structure?: TemplateStepConfig
    generate?: TemplateStepConfig
  }
  basic?: TemplateBasicConfig
  structure?: TemplateStructureConfig
  review?: TemplateReviewConfig
  title_prompt_template?: string
  outline?: TemplateOutlineConfig
}

interface TemplateCardConfig {
  hero?: TemplateHeroConfig
  wizard?: TemplateWizardConfig
}

interface ResolvedOutlineOption {
  key: string
  label: string
  default: boolean
  custom?: boolean
}

const templateTokenPattern = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g

const queryClient = useQueryClient()
const { locale: uiLocale, t } = useI18n()
const route = useRoute()
const router = useRouter()
const companyStore = useCompanyStore()

let competitorKeySeed = 0
let outlineNodeSeed = 0
let allowWizardLeave = false
let pendingLeavePromise: Promise<boolean> | null = null
let pendingLeaveResolve: ((canLeave: boolean) => void) | null = null
const CUSTOM_OUTLINE_MAX_LENGTH = 15

const currentStep = ref(0)
const articleLocale = ref('zh-CN')
const currentArticleId = ref<number | null>(null)
const restoringDraft = ref(false)
const savingDraft = ref(false)
const leaveConfirmOpen = ref(false)
const leaveConfirmBusy = ref(false)

const officialWebsite = ref('')
const brandSummary = ref('')

const primaryQuestionId = ref<number | null>(null)
const supplementalQuestionIds = ref<number[]>([])
const competitorDrafts = ref<DraftCompetitor[]>([])

const assistTitles = ref<string[]>([])
const selectedTitle = ref('')
const customTitle = ref('')
const reviewIntroHook = ref('')
const keyPoints = ref('')
const selectedKnowledgeGroupIds = ref<number[]>([])
const outlineSections = ref<string[]>([])
const customOutlineSections = ref<ResolvedOutlineOption[]>([])
const outlineOptionOrder = ref<string[]>([])
const customOutlineInput = ref('')
const generatedOutline = ref<OutlineDraftNode[]>([])

const assistModalOpen = ref(false)
const assistBusy = ref(false)
const assistProgress = ref(12)
const assistStageIndex = ref(0)
const assistTaskKind = ref<'analyze' | 'title' | 'outline'>('analyze')
const competitorSavingKey = ref<string | null>(null)

const templateId = computed(() => Number(route.query.template_id))
const draftArticleId = computed(() => {
  const value = Number(route.query.article_id)
  return Number.isNaN(value) || value <= 0 ? null : value
})
const enabled = computed(() => !Number.isNaN(templateId.value) && templateId.value > 0)
const draftQueryEnabled = computed(() => enabled.value && draftArticleId.value !== null)

const templateDetailQuery = useQuery({
  queryKey: computed(() => ['templates', 'detail', templateId.value]),
  enabled,
  queryFn: () => templatesApi.detail(templateId.value),
})

const selectedBrand = computed(() => companyStore.currentBrand)
const selectedBrandId = computed(() => selectedBrand.value?.id ?? null)
const normalizedBrandName = computed(() => selectedBrand.value?.name?.trim() || '')

const draftArticleQuery = useQuery({
  queryKey: computed(() => ['articles', 'detail', draftArticleId.value]),
  enabled: draftQueryEnabled,
  queryFn: () => articlesApi.detail(draftArticleId.value as number),
})

const questionsQuery = useQuery({
  queryKey: computed(() => ['brands', selectedBrandId.value, 'questions', 'wizard']),
  enabled: computed(() => enabled.value && Boolean(selectedBrandId.value)),
  queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number),
})

const competitorsQuery = useQuery({
  queryKey: computed(() => ['brands', selectedBrandId.value, 'competitors']),
  enabled: computed(
    () =>
      enabled.value &&
      Boolean(selectedBrandId.value) &&
      templateDetailQuery.data.value?.template_key !== 'research_report' &&
      parseTemplateCardConfig(templateDetailQuery.data.value?.card_config).wizard?.basic?.cards
        ?.competitors?.visible !== false,
  ),
  queryFn: () => brandsApi.listCompetitors(selectedBrandId.value as number),
})

const mutation = useMutation({
  mutationFn: (payload: {
    article_id?: number
    input_params: Record<string, JsonValue>
    wizard_state?: Record<string, JsonValue>
  }) =>
    templatesApi.generate(
      templateId.value,
      normalizeInputParams(payload.input_params, payload.article_id, payload.wizard_state),
    ),
  onSuccess: async () => {
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['articles'] }),
      queryClient.invalidateQueries({ queryKey: ['workspace'] }),
      queryClient.invalidateQueries({ queryKey: ['templates'] }),
    ])
    message.info(t('templates.wizard.messages.queued'))
    allowWizardLeave = true
    await router.replace(resolveExitPath())
  },
  onError: (error) => {
    message.error(formatError(error))
  },
})

const templateDetail = computed(() => templateDetailQuery.data.value)
const templateCardConfig = computed<TemplateCardConfig>(() =>
  parseTemplateCardConfig(templateDetail.value?.card_config),
)
const wizardConfig = computed<TemplateWizardConfig | null>(
  () => templateCardConfig.value.wizard ?? null,
)
const isResearchReportTemplate = computed(
  () => templateDetail.value?.template_key === 'research_report',
)
const templateMeta = computed(() => {
  const fallback = getTemplateMeta(templateDetail.value?.template_key ?? 'unknown')
  return {
    ...fallback,
    accent: templateCardConfig.value.hero?.accent || fallback.accent,
    helper: templateCardConfig.value.hero?.helper || fallback.helper,
  }
})

function findQuestionById(id: number | null): Question | null {
  if (!id) {
    return null
  }
  return questionsQuery.data.value?.find((item) => item.id === id) ?? null
}

const questionOptions = computed(() =>
  (questionsQuery.data.value ?? []).map((item) => ({
    label: item.question_text,
    value: item.id,
  })),
)
const supplementalQuestionOptions = computed(() =>
  questionOptions.value.filter((item) => item.value !== primaryQuestionId.value),
)
const primaryQuestion = computed(() => findQuestionById(primaryQuestionId.value))
const supplementalQuestions = computed(() =>
  supplementalQuestionIds.value
    .map((id) => findQuestionById(id))
    .filter((item): item is Question => Boolean(item)),
)
const primaryQuestionText = computed(() => primaryQuestion.value?.question_text.trim() || '')
const supplementalQuestionTexts = computed(() =>
  supplementalQuestions.value.map((item) => item.question_text.trim()).filter(Boolean),
)
const selectedQuestionTexts = computed(() =>
  dedupeStrings([primaryQuestionText.value, ...supplementalQuestionTexts.value]),
)

const finalTitle = computed(() => customTitle.value.trim() || selectedTitle.value.trim())
const primaryKeyword = computed(() => primaryQuestionText.value)
const showCompetitorsCard = computed(
  () =>
    !isResearchReportTemplate.value &&
    wizardConfig.value?.basic?.cards?.competitors?.visible !== false,
)
const canSaveCompetitorsToLibrary = computed(() => Boolean(selectedBrandId.value))
const canManageCompetitors = computed(() => Boolean(selectedBrandId.value))
const customOutlineMaxLength = computed(
  () => wizardConfig.value?.outline?.custom_max_length ?? CUSTOM_OUTLINE_MAX_LENGTH,
)
const reviewIntroHookConfig = computed<TemplateReviewIntroHookConfig | undefined>(
  () => wizardConfig.value?.structure?.review_intro_hook,
)
const reviewIntroHookOptions = computed<TemplateReviewIntroHookOption[]>(
  () => reviewIntroHookConfig.value?.options ?? [],
)
const reviewIntroHookFieldName = computed(
  () => reviewIntroHookConfig.value?.field_name?.trim() || 'review_intro_hook',
)
const reviewIntroHookRequired = computed(
  () => reviewIntroHookOptions.value.length > 0 && reviewIntroHookConfig.value?.required !== false,
)
const selectedReviewIntroHookOption = computed<TemplateReviewIntroHookOption | null>(
  () => reviewIntroHookOptions.value.find((item) => item.key === reviewIntroHook.value) ?? null,
)
const reviewIntroHookPromptValue = computed(() => {
  const option = selectedReviewIntroHookOption.value
  if (!option) {
    return ''
  }
  return option.description ? `${option.label}：${option.description}` : option.label
})
const templateRenderContext = computed(() =>
  buildTemplateRenderContext({
    locale: articleLocale.value,
    brandName: normalizedBrandName.value,
    officialWebsite: officialWebsite.value,
    brandSummary: brandSummary.value,
    primaryQuestion: primaryQuestionText.value,
    supplementalQuestions: supplementalQuestionTexts.value,
    inputParams: buildStructureInputParams(),
    competitorCount: buildCompetitorPayload().length,
  }),
)

const titleOptions = computed(() => {
  if (assistTitles.value.length > 0) {
    return assistTitles.value
  }
  return buildFallbackTitles(
    templateDetail.value?.template_key,
    normalizedBrandName.value,
    primaryKeyword.value,
    uiLocale.value,
  )
})

const baseOutlineOptions = computed<ResolvedOutlineOption[]>(() => {
  const configured = buildOutlineOptions(
    wizardConfig.value?.outline,
    templateDetail.value?.template_key,
    templateRenderContext.value,
  )
  return [...configured, ...customOutlineSections.value]
})
const outlineOptions = computed<ResolvedOutlineOption[]>(() => {
  const visible = baseOutlineOptions.value.filter((section) => !isBlockedOutlineOption(section))
  return sortOutlineOptions(visible, outlineOptionOrder.value)
})
const selectedOutlineLabels = computed(() =>
  outlineOptions.value
    .filter((section) => outlineSections.value.includes(section.key))
    .map((section) => section.label),
)
const previewTitle = computed(
  () => finalTitle.value || titleOptions.value[0] || templateDetail.value?.template_name || '--',
)
const previewSections = computed(() =>
  outlineOptions.value.filter((section) => outlineSections.value.includes(section.key)),
)
const generatedOutlineNodes = computed(() => serializeOutlineDraftNodes(generatedOutline.value))
const canAddCustomOutline = computed(() => Boolean(wizardConfig.value?.outline?.allow_custom))
const outlinePreviewTitle = computed(
  () => wizardConfig.value?.outline?.preview_title || t('templates.wizard.sections.preview'),
)
const outlinePreviewCaption = computed(
  () => wizardConfig.value?.outline?.preview_caption || t('templates.wizard.hints.preview'),
)
const basicStepCopy = computed<TemplateStepConfig>(() => wizardConfig.value?.steps?.basic ?? {})
const structureStepCopy = computed<TemplateStepConfig>(
  () => wizardConfig.value?.steps?.structure ?? {},
)
const generateStepCopy = computed<TemplateStepConfig>(
  () => wizardConfig.value?.steps?.generate ?? {},
)
const brandCardCopy = computed<TemplateCardCopy>(
  () => sanitizeLegacyKeywordCopy(wizardConfig.value?.basic?.cards?.brand ?? {}),
)
const questionCardCopy = computed<TemplateCardCopy>(
  () => sanitizeQuestionCardCopy(wizardConfig.value?.basic?.cards?.keywords ?? {}),
)
const competitorsCardCopy = computed<TemplateCardCopy>(
  () => wizardConfig.value?.basic?.cards?.competitors ?? {},
)
const titleCardCopy = computed<TemplateCardCopy>(
  () => sanitizeLegacyKeywordCopy(wizardConfig.value?.structure?.cards?.choose_title ?? {}),
)
const outlineCardCopy = computed<TemplateCardCopy>(
  () => wizardConfig.value?.structure?.cards?.outline ?? {},
)
const previewCardCopy = computed<TemplateCardCopy>(
  () => wizardConfig.value?.structure?.cards?.preview ?? {},
)
const analyzeButtonLabel = computed(
  () => t('templates.wizard.actions.analyzeCompetitors'),
)
const showBrandSummaryBox = computed(() => Boolean(selectedBrandId.value))
const reviewAlertMessage = computed(
  () => wizardConfig.value?.review?.alert_message || t('templates.wizard.hints.async'),
)
const isPageLoading = computed(
  () =>
    templateDetailQuery.isPending.value ||
    (draftQueryEnabled.value && draftArticleQuery.isPending.value),
)
const hasWizardDraftContent = computed(
  () =>
    Boolean(selectedBrandId.value) ||
    Boolean(officialWebsite.value.trim()) ||
    Boolean(brandSummary.value.trim()) ||
    Boolean(primaryQuestionId.value) ||
    supplementalQuestionIds.value.length > 0 ||
    competitorDrafts.value.some(hasCompetitorDraftContent) ||
    assistTitles.value.length > 0 ||
    Boolean(customTitle.value.trim()) ||
    Boolean(keyPoints.value.trim()) ||
    selectedKnowledgeGroupIds.value.length > 0 ||
    customOutlineSections.value.some((item) => item.label.trim()) ||
    Boolean(customOutlineInput.value.trim()) ||
    generatedOutline.value.some(hasOutlineDraftContent),
)
const assistStages = computed(() =>
  assistTaskKind.value === 'analyze'
    ? [
        t('templates.wizard.assist.analyzeStages.context'),
        t('templates.wizard.assist.analyzeStages.competitors'),
        t('templates.wizard.assist.analyzeStages.refine'),
      ]
    : assistTaskKind.value === 'title'
      ? [
          t('templates.wizard.assist.titleStages.context'),
          showCompetitorsCard.value
            ? t('templates.wizard.assist.titleStages.strategy')
            : t('templates.wizard.assist.titleStages.strategyNoCompetitors'),
          t('templates.wizard.assist.titleStages.finalize'),
        ]
      : [
          showCompetitorsCard.value
            ? t('templates.wizard.assist.outlineStages.context')
            : t('templates.wizard.assist.outlineStages.contextNoCompetitors'),
          t('templates.wizard.assist.outlineStages.structure'),
          t('templates.wizard.assist.outlineStages.finalize'),
        ],
)
const assistHeading = computed(() =>
  assistTaskKind.value === 'analyze'
    ? t('templates.wizard.assist.analyzeTitle')
    : assistTaskKind.value === 'title'
      ? t('templates.wizard.assist.titleTitle')
      : t('templates.wizard.assist.outlineTitle'),
)

function resetWizardState(detail: NonNullable<typeof templateDetail.value>): void {
  currentArticleId.value = draftArticleId.value
  currentStep.value = 0
  articleLocale.value = 'zh-CN'
  hydrateBrandContextFromCurrentBrand()
  primaryQuestionId.value = null
  supplementalQuestionIds.value = []
  competitorDrafts.value = []
  assistTitles.value = []
  selectedTitle.value = ''
  customTitle.value = ''
  reviewIntroHook.value = ''
  keyPoints.value = ''
  selectedKnowledgeGroupIds.value = []
  customOutlineSections.value = []
  customOutlineInput.value = ''
  generatedOutline.value = []

  const defaults = buildOutlineOptions(
    templateCardConfig.value.wizard?.outline,
    detail.template_key,
    buildTemplateRenderContext({
      locale: 'zh-CN',
    brandName: normalizedBrandName.value,
    officialWebsite: officialWebsite.value,
    brandSummary: brandSummary.value,
      primaryQuestion: '',
      supplementalQuestions: [],
      inputParams: {},
      competitorCount: 0,
    }),
  )
    .filter((section) => section.default && !isBlockedOutlineOption(section))
    .map((section) => section.key)
  outlineOptionOrder.value = buildOutlineOptions(
    templateCardConfig.value.wizard?.outline,
    detail.template_key,
    buildTemplateRenderContext({
      locale: 'zh-CN',
    brandName: normalizedBrandName.value,
    officialWebsite: officialWebsite.value,
    brandSummary: brandSummary.value,
      primaryQuestion: '',
      supplementalQuestions: [],
      inputParams: {},
      competitorCount: 0,
    }),
  )
    .filter((section) => !isBlockedOutlineOption(section))
    .map((section) => section.key)
  outlineSections.value = defaults
}

function applyDraftArticleState(detail: ArticleDetail): void {
  const draftState = asRecord(detail.wizard_state)
  currentArticleId.value = detail.id
  if (!draftState) {
    return
  }

  restoringDraft.value = true

  currentStep.value = clampStep(numberFromUnknown(draftState.current_step))
  articleLocale.value = stringFromUnknown(draftState.locale) || articleLocale.value
  hydrateBrandContextFromCurrentBrand()
  officialWebsite.value = stringFromUnknown(draftState.official_website) || officialWebsite.value
  brandSummary.value = stringFromUnknown(draftState.brand_summary) || brandSummary.value
  primaryQuestionId.value = numberFromUnknown(draftState.primary_question_id) ?? null
  supplementalQuestionIds.value =
    numberListFromUnknown(draftState.supplemental_question_ids)?.slice(0, 3) ?? []
  competitorDrafts.value = parseDraftCompetitors(draftState.competitor_drafts)
  assistTitles.value = stringListFromUnknown(draftState.assist_titles) ?? []
  selectedTitle.value = stringFromUnknown(draftState.selected_title) || ''
  customTitle.value = stringFromUnknown(draftState.custom_title) || ''
  reviewIntroHook.value = stringFromUnknown(draftState.review_intro_hook) || ''
  keyPoints.value = stringFromUnknown(draftState.key_points) || ''
  selectedKnowledgeGroupIds.value = numberListFromUnknown(draftState.knowledge_group_ids) ?? []
  customOutlineSections.value = parseCustomOutlineSections(draftState.custom_outline_sections)
  outlineOptionOrder.value =
    sanitizeOutlineKeys(stringListFromUnknown(draftState.outline_option_order)) ?? outlineOptionOrder.value
  outlineSections.value =
    sanitizeOutlineKeys(stringListFromUnknown(draftState.outline_sections)) ?? outlineSections.value
  generatedOutline.value = decorateOutlineNodes(parseOutlineNodes(draftState.generated_outline))

  restoringDraft.value = false
}

watch(
  [enabled, templateDetail, () => draftArticleQuery.data.value],
  ([isOpen, detail, draftArticle]) => {
    if (!isOpen || !detail) {
      return
    }

    resetWizardState(detail)
    if (draftArticle && draftArticle.generate_status === 'draft') {
      applyDraftArticleState(draftArticle)
    }
  },
  { immediate: true },
)

watch(
  () => selectedBrand.value,
  () => {
    hydrateBrandContextFromCurrentBrand()
  },
  { immediate: true },
)

watch(
  baseOutlineOptions,
  (options) => {
    outlineOptionOrder.value = syncOutlineOptionOrder(
      outlineOptionOrder.value,
      options.map((item) => item.key),
    )
  },
  { immediate: true },
)

watch(
  titleOptions,
  (options) => {
    if (!selectedTitle.value && options.length > 0) {
      selectedTitle.value = options[0]
    }
  },
  { immediate: true },
)

watch(
  reviewIntroHookOptions,
  (options) => {
    if (options.some((item) => item.key === reviewIntroHook.value)) {
      return
    }
    const fallback = options.find((item) => item.default) ?? options[0]
    reviewIntroHook.value = fallback?.key ?? ''
  },
  { immediate: true },
)

watch(primaryQuestionId, (id) => {
  if (!id) {
    return
  }
  supplementalQuestionIds.value = supplementalQuestionIds.value.filter((item) => item !== id)
})

watch(supplementalQuestionIds, (ids) => {
  const optionValues = supplementalQuestionOptions.value.map((item) => item.value)
  const allowedIds = new Set(optionValues)
  const shouldCheckOptions = optionValues.length > 0
  const normalized = ids.filter(
    (id, index) =>
      id !== primaryQuestionId.value &&
      ids.indexOf(id) === index &&
      (!shouldCheckOptions || allowedIds.has(id)),
  )
  const limited = normalized.slice(0, 3)
  if (limited.length !== ids.length || limited.some((id, index) => id !== ids[index])) {
    supplementalQuestionIds.value = limited
  }
})

watch(selectedBrandId, async (brandId) => {
  if (restoringDraft.value) {
    return
  }
  if (!brandId) {
    competitorDrafts.value = competitorDrafts.value.map((item) => ({
      ...item,
      libraryId: undefined,
      saved: false,
    }))
    return
  }

  const brand = selectedBrand.value
  if (brand?.website && !officialWebsite.value.trim()) {
    officialWebsite.value = brand.website
  }
  if (brand?.description && !brandSummary.value.trim()) {
    brandSummary.value = brand.description
  }

  primaryQuestionId.value = null
  supplementalQuestionIds.value = []
  await questionsQuery.refetch()

  if (!showCompetitorsCard.value) {
    competitorDrafts.value = []
    return
  }

  const competitorsResult = await competitorsQuery.refetch()
  competitorDrafts.value = (competitorsResult.data ?? []).map((item) => ({
    key: nextCompetitorKey(),
    libraryId: item.id,
    name: item.name,
    website: item.website ?? '',
    description: item.description ?? '',
    saved: true,
  }))
}, { immediate: true })

function hydrateBrandContextFromCurrentBrand(): void {
  const brand = selectedBrand.value
  officialWebsite.value = brand?.website ?? ''
  brandSummary.value = brand?.description ?? ''
}

onBeforeRouteLeave(async () => {
  if (!shouldConfirmBeforeLeavingWizard()) {
    return true
  }
  return requestLeaveConfirmation()
})

onMounted(() => {
  window.addEventListener('beforeunload', handleBeforeUnload)
})

onBeforeUnmount(() => {
  window.removeEventListener('beforeunload', handleBeforeUnload)
})

function shouldConfirmBeforeLeavingWizard(): boolean {
  return enabled.value && !allowWizardLeave && hasWizardDraftContent.value
}

function requestLeaveConfirmation(): Promise<boolean> {
  if (pendingLeavePromise) {
    return pendingLeavePromise
  }

  leaveConfirmOpen.value = true
  pendingLeavePromise = new Promise<boolean>((resolve) => {
    pendingLeaveResolve = resolve
  })
  return pendingLeavePromise
}

function resolveLeaveConfirmation(canLeave: boolean): void {
  leaveConfirmOpen.value = false
  leaveConfirmBusy.value = false
  const resolve = pendingLeaveResolve
  pendingLeavePromise = null
  pendingLeaveResolve = null
  resolve?.(canLeave)
}

async function handleSaveDraftAndLeave(): Promise<void> {
  if (leaveConfirmBusy.value) {
    return
  }

  leaveConfirmBusy.value = true
  const saved = await saveWizardDraft()
  if (saved) {
    allowWizardLeave = true
    resolveLeaveConfirmation(true)
    return
  }
  leaveConfirmBusy.value = false
}

function handleDiscardAndLeave(): void {
  allowWizardLeave = true
  resolveLeaveConfirmation(true)
}

function handleStayOnWizard(): void {
  resolveLeaveConfirmation(false)
}

function handleBeforeUnload(event: BeforeUnloadEvent): void {
  if (!shouldConfirmBeforeLeavingWizard()) {
    return
  }

  event.preventDefault()
  event.returnValue = ''
}

function hasCompetitorDraftContent(item: DraftCompetitor): boolean {
  return Boolean(item.name.trim() || item.website.trim() || item.description.trim())
}

function hasOutlineDraftContent(item: OutlineDraftNode): boolean {
  return Boolean(item.outline.trim()) || item.children.some(hasOutlineDraftContent)
}

function nextCompetitorKey(): string {
  competitorKeySeed += 1
  return `competitor-${competitorKeySeed}`
}

function nextOutlineNodeKey(): string {
  outlineNodeSeed += 1
  return `outline-${outlineNodeSeed}`
}

function validateBasicInfo(): boolean {
  if (!normalizedBrandName.value) {
    message.warning(t('templates.wizard.messages.missingBrand'))
    return false
  }
  if (!selectedBrandId.value) {
    message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
    return false
  }
  if (!primaryQuestionId.value || !primaryQuestionText.value) {
    message.warning(t('templates.wizard.messages.missingPrimaryQuestion'))
    return false
  }

  return true
}

function validateAnalyzeInfo(): boolean {
  if (!canManageCompetitors.value) {
    message.warning(t('templates.wizard.messages.selectBrandBeforeCompetitors'))
    return false
  }
  return true
}

function validateStructure(): boolean {
  if (!finalTitle.value) {
    message.warning(t('templates.wizard.messages.missingTitle'))
    return false
  }
  if (reviewIntroHookRequired.value && !reviewIntroHook.value) {
    message.warning(t('templates.wizard.messages.missingReviewIntroHook'))
    return false
  }
  if (outlineSections.value.length === 0) {
    message.warning(t('templates.wizard.messages.missingOutline'))
    return false
  }
  return true
}

function toggleOutlineSection(key: string, checked: boolean): void {
  if (checked) {
    if (!outlineSections.value.includes(key)) {
      outlineSections.value = [...outlineSections.value, key]
    }
    return
  }

  outlineSections.value = outlineSections.value.filter((item) => item !== key)
}

function validateGeneratedOutline(): boolean {
  if (generatedOutlineNodes.value.length === 0) {
    message.warning(t('templates.wizard.messages.missingGeneratedOutline'))
    return false
  }
  return true
}

async function handleNext(): Promise<void> {
  if (currentStep.value === 0) {
    if (!validateBasicInfo()) {
      return
    }
    await runTitleTask()
    return
  }

  if (currentStep.value === 1) {
    if (!validateStructure()) {
      return
    }
    await runOutlineTask()
    return
  }
}

function handlePrev(): void {
  currentStep.value = Math.max(0, currentStep.value - 1)
}

async function handleAnalyze(): Promise<void> {
  if (!validateAnalyzeInfo()) {
    return
  }
  await runAnalyzeTask(true)
}

function beginAssistTask(kind: 'analyze' | 'title' | 'outline'): void {
  assistTaskKind.value = kind
  if (assistBusy.value) {
    return
  }
  assistBusy.value = true
  assistModalOpen.value = true
  assistProgress.value = 12
  assistStageIndex.value = 0
}

function endAssistTask(): void {
  void queryClient.invalidateQueries({ queryKey: ['workspace'] })
  window.setTimeout(() => {
    assistModalOpen.value = false
    assistBusy.value = false
  }, 180)
}

async function runAnalyzeTask(notifySuccess: boolean): Promise<void> {
  if (assistBusy.value) {
    return
  }

  beginAssistTask('analyze')
  try {
    const competitorPayload = buildCompetitorPayload()
    const task = await templatesApi.createAnalyzeTask(templateId.value, {
      locale: articleLocale.value,
      brand_id: selectedBrandId.value,
      brand_name: normalizedBrandName.value,
      website: officialWebsite.value.trim() || null,
      brand_question: primaryQuestionText.value,
      input_params: buildAssistInputParams(),
      existing_competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
    })

    const result = await pollAnalyzeTask(task.task_id)
    applyAnalyzeResult(result)
    assistProgress.value = 100
    assistStageIndex.value = assistStages.value.length - 1

    if (notifySuccess) {
      message.success(t('templates.wizard.messages.assistReady'))
    }
  } catch (error) {
    message.error(formatError(error))
  } finally {
    endAssistTask()
  }
}

async function runTitleTask(): Promise<void> {
  if (assistBusy.value) {
    return
  }

  beginAssistTask('title')
  try {
    const competitorPayload = buildCompetitorPayload()
    const task = await templatesApi.createTitleTask(templateId.value, {
      locale: articleLocale.value,
      brand_id: selectedBrandId.value,
      brand_name: normalizedBrandName.value,
      website: officialWebsite.value.trim() || null,
      brand_summary: brandSummary.value.trim() || null,
      brand_question: primaryQuestionText.value,
      input_params: buildTitleInputParams(),
      keywords: primaryQuestionText.value ? [primaryQuestionText.value] : [],
      competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
    })

    const titles = await pollTitleTask(task.task_id)
    assistTitles.value = dedupeStrings(titles).slice(0, 5)
    if (assistTitles.value.length > 0 && !assistTitles.value.includes(selectedTitle.value)) {
      selectedTitle.value = assistTitles.value[0]
    }
    assistProgress.value = 100
    assistStageIndex.value = assistStages.value.length - 1
    currentStep.value = 1
  } catch (error) {
    message.error(formatError(error))
  } finally {
    endAssistTask()
  }
}

async function runOutlineTask(): Promise<void> {
  if (assistBusy.value) {
    return
  }

  beginAssistTask('outline')
  try {
    const competitorPayload = buildCompetitorPayload()
    const task = await templatesApi.createOutlineTask(templateId.value, {
      locale: articleLocale.value,
      brand_id: selectedBrandId.value,
      brand_name: normalizedBrandName.value,
      website: officialWebsite.value.trim() || null,
      brand_summary: brandSummary.value.trim() || null,
      title: finalTitle.value,
      brand_question: primaryQuestionText.value,
      supplemental_questions: supplementalQuestionTexts.value,
      input_params: buildStructureInputParams(),
      keywords: selectedQuestionTexts.value,
      competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
      outline_sections: selectedOutlineLabels.value,
    })

    const outline = await pollOutlineTask(task.task_id)
    generatedOutline.value = decorateOutlineNodes(outline)
    assistProgress.value = 100
    assistStageIndex.value = assistStages.value.length - 1
    currentStep.value = 2
  } catch (error) {
    message.error(formatError(error))
  } finally {
    endAssistTask()
  }
}

const ASSIST_POLL_INTERVAL_MS = 3000
const ASSIST_POLL_MAX_ATTEMPTS = 40

async function pollAnalyzeTask(taskId: string): Promise<TemplateAnalyzeResult> {
  for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
    const task = await templatesApi.getAnalyzeTaskResult(templateId.value, taskId)
    advanceAssistProgress(attempt, task.status)

    if (task.status === 'completed' && task.result) {
      return task.result
    }

    if (task.status === 'failed') {
      throw new Error(task.error_message || t('templates.wizard.messages.assistFailed'))
    }

    await sleep(ASSIST_POLL_INTERVAL_MS)
  }

  throw new Error(t('templates.wizard.messages.assistTimeout'))
}

async function pollTitleTask(taskId: string): Promise<string[]> {
  for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
    const task = await templatesApi.getTitleTaskResult(templateId.value, taskId)
    advanceAssistProgress(attempt, task.status)

    if (task.status === 'completed' && task.result) {
      return task.result
    }

    if (task.status === 'failed') {
      throw new Error(task.error_message || t('templates.wizard.messages.titleFailed'))
    }

    await sleep(ASSIST_POLL_INTERVAL_MS)
  }

  throw new Error(t('templates.wizard.messages.titleTimeout'))
}

async function pollOutlineTask(taskId: string): Promise<TemplateOutlineNode[]> {
  for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
    const task = await templatesApi.getOutlineTaskResult(templateId.value, taskId)
    advanceAssistProgress(attempt, task.status)

    if (task.status === 'completed' && task.result) {
      return task.result
    }

    if (task.status === 'failed') {
      throw new Error(task.error_message || t('templates.wizard.messages.outlineFailed'))
    }

    await sleep(ASSIST_POLL_INTERVAL_MS)
  }

  throw new Error(t('templates.wizard.messages.outlineTimeout'))
}

function advanceAssistProgress(attempt: number, status: string): void {
  if (status === 'running') {
    assistStageIndex.value = Math.min(assistStages.value.length - 1, Math.floor((attempt + 1) / 2))
  }

  assistProgress.value = Math.min(92, 24 + attempt * 6)
}

function applyAnalyzeResult(result: TemplateAnalyzeResult): void {
  if (result.brand_summary?.trim()) {
    brandSummary.value = result.brand_summary.trim()
  }

  if (showCompetitorsCard.value) {
    competitorDrafts.value = mergeCompetitors(competitorDrafts.value, result.competitors ?? [])
  }
}

function addCompetitorRow(): void {
  if (!canManageCompetitors.value) {
    message.warning(t('templates.wizard.messages.selectBrandBeforeCompetitors'))
    return
  }
  competitorDrafts.value = [
    ...competitorDrafts.value,
    {
      key: nextCompetitorKey(),
      name: '',
      website: '',
      description: '',
      saved: false,
    },
  ]
}

function removeCompetitorRow(key: string): void {
  competitorDrafts.value = competitorDrafts.value.filter((item) => item.key !== key)
}

async function handleFavoriteCompetitor(item: DraftCompetitor): Promise<void> {
  const brandId = selectedBrandId.value
  if (!brandId) {
    message.warning(t('templates.wizard.messages.selectBrandBeforeFavorite'))
    return
  }

  competitorSavingKey.value = item.key

  try {
    if (item.saved && item.libraryId) {
      await brandsApi.removeCompetitor(brandId, item.libraryId)
      item.libraryId = undefined
      item.saved = false
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ['brands'] }),
        queryClient.invalidateQueries({ queryKey: ['brands', brandId, 'competitors'] }),
      ])
      message.success(t('templates.wizard.messages.unsavedCompetitor'))
      return
    }

    if (!item.name.trim()) {
      message.warning(t('templates.wizard.messages.missingCompetitorName'))
      return
    }

    const payload = {
      name: item.name.trim(),
      website: item.website.trim() || null,
      description: item.description.trim() || null,
      product_lines_json: null,
    }

    if (item.libraryId) {
      await brandsApi.updateCompetitor(brandId, item.libraryId, payload)
    } else {
      const created = await brandsApi.createCompetitor(brandId, payload)
      item.libraryId = created.id
    }

    item.saved = true
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['brands'] }),
      queryClient.invalidateQueries({ queryKey: ['brands', brandId, 'competitors'] }),
    ])
    message.success(t('templates.wizard.messages.savedCompetitor'))
  } catch (error) {
    message.error(formatError(error))
  } finally {
    competitorSavingKey.value = null
  }
}

function buildCompetitorPayload(): TemplateAssistCompetitor[] {
  if (!showCompetitorsCard.value) {
    return []
  }

  return competitorDrafts.value
    .filter((item) => item.name.trim() || item.website.trim())
    .map<TemplateAssistCompetitor>((item) => ({
      name: item.name.trim(),
      website: item.website.trim() || null,
      description: item.description.trim() || null,
    }))
}

function sanitizeQuestionCardCopy(copy: TemplateCardCopy): TemplateCardCopy {
  if (copy.title?.includes('关键词') || copy.hint?.includes('关键词')) {
    return {
      ...copy,
      title: undefined,
      hint: undefined,
    }
  }
  return {
    ...copy,
    title: sanitizeLegacyKeywordText(copy.title) || undefined,
    hint: sanitizeLegacyKeywordText(copy.hint) || undefined,
  }
}

function sanitizeLegacyKeywordCopy(copy: TemplateCardCopy): TemplateCardCopy {
  return {
    ...copy,
    title: sanitizeLegacyKeywordText(copy.title) || undefined,
    hint: sanitizeLegacyKeywordText(copy.hint) || undefined,
  }
}

function sanitizeLegacyKeywordText(value: string | undefined): string {
  return (value || '')
    .replace(/品牌主问题/g, '优化关键词')
    .replace(/补充覆盖问题/g, '补充关键词')
    .replace(/核心关键词/g, '优化关键词')
    .replace(/标题生成/g, '后续生成')
}

function buildAssistInputParams(): Record<string, JsonValue> {
  const payload: Record<string, JsonValue> = {
    brand_question: primaryQuestionText.value,
    primary_question: primaryQuestionText.value,
    primary_keyword: primaryQuestionText.value,
  }
  const derivedInputs = wizardConfig.value?.derived_inputs ?? {}
  for (const [key, config] of Object.entries(derivedInputs)) {
    const value = resolveDerivedInputValue(key, config)
    if (value !== null && value !== '' && value !== undefined) {
      payload[key] = value
    }
  }

  return payload
}

function buildTitleInputParams(): Record<string, JsonValue> {
  return buildAssistInputParams()
}

function buildStructureInputParams(): Record<string, JsonValue> {
  const payload = buildAssistInputParams()
  if (supplementalQuestionTexts.value.length > 0) {
    payload.supplemental_questions = supplementalQuestionTexts.value
    payload.keywords = selectedQuestionTexts.value
  }
  if (reviewIntroHookPromptValue.value) {
    payload[reviewIntroHookFieldName.value] = reviewIntroHookPromptValue.value
  }
  return payload
}

function buildPayload(): Record<string, JsonValue> {
  const competitorPayload = buildCompetitorPayload()
  const competitorInputValue =
    competitorPayload.length > 0
      ? competitorPayload.map((item) => ({
          name: item.name,
          website: item.website ?? null,
          description: item.description ?? null,
        }))
      : undefined
  const payload: Record<string, JsonValue | undefined> = {
    locale: articleLocale.value,
    title: finalTitle.value || undefined,
    outline_sections: selectedOutlineLabels.value,
    article_outline:
      generatedOutlineNodes.value.length > 0
        ? serializeOutlinePayloadNodes(generatedOutlineNodes.value)
        : undefined,
    key_points: keyPoints.value.trim() || undefined,
    brand_id: selectedBrandId.value ?? undefined,
    brand_name: normalizedBrandName.value || undefined,
    brand_summary: brandSummary.value.trim() || undefined,
    official_website: officialWebsite.value.trim() || undefined,
    brand_question: primaryQuestionText.value || undefined,
    primary_question: primaryQuestionText.value || undefined,
    supplemental_questions:
      supplementalQuestionTexts.value.length > 0 ? supplementalQuestionTexts.value : undefined,
    primary_keyword: primaryQuestionText.value || undefined,
    keywords: selectedQuestionTexts.value.length > 0 ? selectedQuestionTexts.value : undefined,
    [reviewIntroHookFieldName.value]: reviewIntroHookPromptValue.value || undefined,
    knowledge_group_ids:
      selectedKnowledgeGroupIds.value.length > 0 ? selectedKnowledgeGroupIds.value : undefined,
    competitors: competitorInputValue,
  }

  const derivedInputs = wizardConfig.value?.derived_inputs ?? {}
  const derivedKeys = Object.keys(derivedInputs)
  if (derivedKeys.length > 0) {
    for (const key of derivedKeys) {
      const derivedValue = resolveDerivedInputValue(key, derivedInputs[key])
      if (derivedValue !== undefined) {
        payload[key] = derivedValue
      }
    }
  }

  return Object.fromEntries(
    Object.entries(payload).filter(([, value]) => value !== undefined && value !== ''),
  ) as Record<string, JsonValue>
}

function buildDraftState(): Record<string, JsonValue> {
  return {
    current_step: currentStep.value,
    locale: articleLocale.value,
    selected_brand_id: selectedBrandId.value,
    brand_name: normalizedBrandName.value,
    official_website: officialWebsite.value.trim(),
    brand_summary: brandSummary.value.trim(),
    primary_question_id: primaryQuestionId.value,
    primary_question_text: primaryQuestionText.value,
    supplemental_question_ids: supplementalQuestionIds.value,
    supplemental_question_texts: supplementalQuestionTexts.value,
    competitor_drafts: showCompetitorsCard.value
      ? competitorDrafts.value.map((item) => ({
          library_id: item.libraryId ?? null,
          name: item.name.trim(),
          website: item.website.trim(),
          description: item.description.trim(),
          saved: item.saved,
        }))
      : [],
    assist_titles: assistTitles.value,
    selected_title: selectedTitle.value,
    custom_title: customTitle.value,
    title: finalTitle.value || selectedTitle.value || '',
    review_intro_hook: reviewIntroHook.value,
    knowledge_group_ids: selectedKnowledgeGroupIds.value,
    outline_sections: sanitizeOutlineKeys(outlineSections.value) ?? [],
    outline_option_order: sanitizeOutlineKeys(outlineOptionOrder.value) ?? [],
    custom_outline_sections: customOutlineSections.value.map((item) => ({
      key: item.key,
      label: item.label,
      default: item.default,
      custom: item.custom ?? false,
    })),
    generated_outline: serializeOutlinePayloadNodes(generatedOutlineNodes.value),
    key_points: keyPoints.value.trim(),
  }
}

function handleGenerate(): void {
  if (!validateBasicInfo() || !validateStructure() || !validateGeneratedOutline()) {
    return
  }
  mutation.mutate({
    article_id: currentArticleId.value ?? undefined,
    input_params: buildPayload(),
    wizard_state: buildDraftState(),
  })
}

async function saveWizardDraft(): Promise<boolean> {
  if (!enabled.value || savingDraft.value) {
    return false
  }

  savingDraft.value = true
  try {
    const result = await templatesApi.saveDraft(templateId.value, {
      article_id: currentArticleId.value ?? undefined,
      current_step: currentStep.value,
      wizard_state: buildDraftState(),
    })
    currentArticleId.value = result.article_id
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['articles'] }),
      queryClient.invalidateQueries({ queryKey: ['workspace'] }),
    ])
    message.success(t('templates.wizard.messages.draftSaved'))
    return true
  } catch (error) {
    message.error(formatError(error) || t('templates.wizard.messages.draftSaveFailed'))
    return false
  } finally {
    savingDraft.value = false
  }
}

function resolveExitPath(): string {
  return route.query.from === 'workspace' ? '/workspace' : '/articles/templates'
}

async function handleSaveDraftAndExit(): Promise<void> {
  await router.replace(resolveExitPath())
}

function dedupeStrings(values: string[]): string[] {
  const seen = new Set<string>()
  return values
    .map((item) => item.trim())
    .filter((item) => {
      if (!item) {
        return false
      }
      const key = item.toLowerCase()
      if (seen.has(key)) {
        return false
      }
      seen.add(key)
      return true
    })
}

function mergeCompetitors(
  existing: DraftCompetitor[],
  incoming: TemplateAssistCompetitor[],
): DraftCompetitor[] {
  const next = [...existing]
  const indexMap = new Map<string, number>()

  next.forEach((item, index) => {
    indexMap.set(competitorIdentity(item.name, item.website), index)
  })

  for (const item of incoming) {
    const identity = competitorIdentity(item.name ?? '', item.website ?? '')
    if (!identity) {
      continue
    }

    const existingIndex = indexMap.get(identity)
    if (existingIndex !== undefined) {
      const target = next[existingIndex]
      if (!target.website && item.website) {
        target.website = item.website
      }
      if (!target.description && item.description) {
        target.description = item.description
      }
      continue
    }

    next.push({
      key: nextCompetitorKey(),
      name: item.name ?? '',
      website: item.website ?? '',
      description: item.description ?? '',
      saved: false,
    })
    indexMap.set(identity, next.length - 1)
  }

  return next
}

function competitorIdentity(name: string, website: string): string {
  return (website.trim() || name.trim()).toLowerCase()
}

function addCustomOutlineSection(): void {
  const label = customOutlineInput.value.trim()
  if (!label) {
    return
  }
  if (Array.from(label).length > customOutlineMaxLength.value) {
    message.warning(
      t('templates.wizard.messages.customOutlineTooLong', { count: customOutlineMaxLength.value }),
    )
    return
  }

  const key = `custom-${Date.now()}`
  customOutlineSections.value = [
    ...customOutlineSections.value,
    { key, label, default: true, custom: true },
  ]
  outlineOptionOrder.value = [...outlineOptionOrder.value, key]
  outlineSections.value = [...outlineSections.value, key]
  customOutlineInput.value = ''
}

function removeOutlineSection(key: string): void {
  outlineSections.value = outlineSections.value.filter((item) => item !== key)

  const target = outlineOptions.value.find((item) => item.key === key)
  if (!target?.custom) {
    return
  }

  customOutlineSections.value = customOutlineSections.value.filter((item) => item.key !== key)
  outlineOptionOrder.value = outlineOptionOrder.value.filter((item) => item !== key)
}

function sortOutlineOptions(
  options: ResolvedOutlineOption[],
  order: string[],
): ResolvedOutlineOption[] {
  if (options.length === 0) {
    return []
  }

  const optionMap = new Map(options.map((item) => [item.key, item]))
  const ordered: ResolvedOutlineOption[] = []

  for (const key of order) {
    const option = optionMap.get(key)
    if (!option) {
      continue
    }
    ordered.push(option)
    optionMap.delete(key)
  }

  for (const option of options) {
    if (optionMap.has(option.key)) {
      ordered.push(option)
    }
  }

  return ordered
}

function syncOutlineOptionOrder(current: string[], nextKeys: string[]): string[] {
  const nextSet = new Set(nextKeys)
  const ordered = current.filter((key) => nextSet.has(key))
  const orderedSet = new Set(ordered)

  for (const key of nextKeys) {
    if (!orderedSet.has(key)) {
      ordered.push(key)
    }
  }

  return ordered
}

function decorateOutlineNodes(nodes: TemplateOutlineNode[]): OutlineDraftNode[] {
  return nodes
    .map((node) => decorateOutlineNode(node))
    .filter((node): node is OutlineDraftNode => node !== null)
}

function decorateOutlineNode(node: TemplateOutlineNode): OutlineDraftNode | null {
  const outline = node.outline.trim()
  if (!outline) {
    return null
  }
  return {
    id: nextOutlineNodeKey(),
    outline,
    children: (node.children ?? [])
      .map((child) => decorateOutlineNode(child))
      .filter((child): child is OutlineDraftNode => child !== null),
  }
}

function serializeOutlineDraftNodes(nodes: OutlineDraftNode[]): TemplateOutlineNode[] {
  return nodes
    .map((node) => serializeOutlineDraftNode(node))
    .filter((node): node is TemplateOutlineNode => node !== null)
}

function serializeOutlineDraftNode(node: OutlineDraftNode): TemplateOutlineNode | null {
  const outline = node.outline.trim()
  if (!outline) {
    return null
  }
  const children = serializeOutlineDraftNodes(node.children)
  return children.length > 0 ? { outline, children } : { outline }
}

function serializeOutlinePayloadNodes(nodes: TemplateOutlineNode[]): JsonValue[] {
  return nodes.map((node) => serializeOutlinePayloadNode(node))
}

function serializeOutlinePayloadNode(node: TemplateOutlineNode): JsonValue {
  const payload: Record<string, JsonValue> = {
    outline: node.outline,
  }
  if (node.children && node.children.length > 0) {
    payload.children = serializeOutlinePayloadNodes(node.children)
  }
  return payload
}

function addOutlineRootNode(): void {
  const node = createEmptyOutlineNode()
  generatedOutline.value = [...generatedOutline.value, node]
  focusOutlineNode(node.id)
}

function addOutlineNodeAfter(targetId: string): void {
  const node = createEmptyOutlineNode()
  generatedOutline.value = insertOutlineSibling(generatedOutline.value, targetId, node)
  focusOutlineNode(node.id)
}

function addOutlineChildNode(targetId: string): void {
  const node = createEmptyOutlineNode()
  generatedOutline.value = insertOutlineChild(generatedOutline.value, targetId, node)
  focusOutlineNode(node.id)
}

function removeOutlineNode(targetId: string): void {
  generatedOutline.value = deleteOutlineNode(generatedOutline.value, targetId)
}

function createEmptyOutlineNode(): OutlineDraftNode {
  return { id: nextOutlineNodeKey(), outline: '', children: [] }
}

async function focusOutlineNode(nodeId: string): Promise<void> {
  await nextTick()
  window.requestAnimationFrame(() => {
    const target = document.querySelector<HTMLElement>(`[data-outline-node-id="${nodeId}"]`)
    target?.scrollIntoView({ behavior: 'smooth', block: 'center' })
    target?.focus({ preventScroll: true })
  })
}

function insertOutlineSibling(
  nodes: OutlineDraftNode[],
  targetId: string,
  insertedNode: OutlineDraftNode,
): OutlineDraftNode[] {
  const next: OutlineDraftNode[] = []
  for (const node of nodes) {
    const current: OutlineDraftNode = {
      ...node,
      children: insertOutlineSibling(node.children, targetId, insertedNode),
    }
    next.push(current)
    if (node.id === targetId) {
      next.push(insertedNode)
    }
  }
  return next
}

function insertOutlineChild(
  nodes: OutlineDraftNode[],
  targetId: string,
  insertedNode: OutlineDraftNode,
): OutlineDraftNode[] {
  return nodes.map((node) => {
    if (node.id === targetId) {
      return {
        ...node,
        children: [...node.children, insertedNode],
      }
    }
    return {
      ...node,
      children: insertOutlineChild(node.children, targetId, insertedNode),
    }
  })
}

function deleteOutlineNode(nodes: OutlineDraftNode[], targetId: string): OutlineDraftNode[] {
  return nodes
    .filter((node) => node.id !== targetId)
    .map((node) => ({
      ...node,
      children: deleteOutlineNode(node.children, targetId),
    }))
}

function parseTemplateCardConfig(
  raw: Record<string, JsonValue> | null | undefined,
): TemplateCardConfig {
  if (!raw || typeof raw !== 'object') {
    return {}
  }

  const hero = asRecord(raw.hero)
  const wizard = asRecord(raw.wizard)
  const structure = asRecord(wizard?.structure)
  const outline = asRecord(structure?.outline ?? wizard?.outline)
  const steps = asRecord(wizard?.steps)
  const basic = asRecord(wizard?.basic)
  const review = asRecord(wizard?.review)
  const sectionsRaw = Array.isArray(outline?.sections) ? outline?.sections : []
  const basicCards = asRecord(basic?.cards)
  const structureCards = asRecord(structure?.cards)

  return {
    hero: hero
      ? {
          accent: stringFromUnknown(hero.accent),
          helper: stringFromUnknown(hero.helper),
        }
      : undefined,
    wizard: wizard
      ? {
          managed_fields: stringListFromUnknown(wizard.managed_fields),
          derived_inputs: readDerivedInputs(wizard.derived_inputs),
          steps: steps
            ? {
                basic: readStepConfig(steps.basic),
                structure: readStepConfig(steps.structure),
                generate: readStepConfig(steps.generate),
              }
            : undefined,
          basic: basic
            ? {
                analyze_button_label: stringFromUnknown(basic.analyze_button_label),
                cards: basicCards
                  ? {
                      brand: readCardCopy(basicCards.brand),
                      keywords: readCardCopy(basicCards.keywords),
                      template_fields: readCardCopy(basicCards.template_fields),
                      competitors: readCardCopy(basicCards.competitors),
                    }
                  : undefined,
              }
            : undefined,
          structure: structure
            ? {
                cards: structureCards
                  ? {
                      choose_title: readCardCopy(structureCards.choose_title),
                      outline: readCardCopy(structureCards.outline),
                      preview: readCardCopy(structureCards.preview),
                    }
                  : undefined,
                review_intro_hook: readReviewIntroHookConfig(structure.review_intro_hook),
              }
            : undefined,
          review: review
            ? {
                card: readCardCopy(review.card),
                alert_message: stringFromUnknown(review.alert_message),
              }
            : undefined,
          title_prompt_template:
            stringFromUnknown(structure?.title_prompt_template) ||
            stringFromUnknown(wizard.title_prompt_template),
          outline: outline
            ? {
                allow_custom: Boolean(outline.allow_custom),
                custom_placeholder: stringFromUnknown(outline.custom_placeholder),
                custom_max_length:
                  typeof outline.custom_max_length === 'number' && outline.custom_max_length > 0
                    ? outline.custom_max_length
                    : undefined,
                preview_title: stringFromUnknown(outline.preview_title),
                preview_caption: stringFromUnknown(outline.preview_caption),
                sections: sectionsRaw
                  .map((item, index) => ({ item: asRecord(item), index }))
                  .filter((entry): entry is { item: Record<string, unknown>; index: number } =>
                    Boolean(entry.item),
                  )
                  .map(({ item, index }) => ({
                    key: stringFromUnknown(item.key) || `section-${index + 1}`,
                    label: stringFromUnknown(item.label),
                    label_template: stringFromUnknown(item.label_template),
                    default: Boolean(item.default),
                  })),
              }
            : undefined,
        }
      : undefined,
  }
}

function buildOutlineOptions(
  config: TemplateOutlineConfig | undefined,
  templateKey: string | undefined,
  context: Record<string, string>,
): ResolvedOutlineOption[] {
  const configured = (config?.sections ?? [])
    .map((section) => {
      const label =
        resolveTemplateText(section.label_template, context) || section.label || section.key
      return {
        key: section.key,
        label,
        default: Boolean(section.default),
      }
    })
    .filter((section) => section.label.trim() !== '')

  if (configured.length > 0) {
    return configured
  }

  const defaults = buildDefaultOutlineOptions(templateKey, context.locale || 'zh-CN')
  return defaults.map((label, index) => ({
    key: `default-${index}`,
    label,
    default: index < Math.min(3, defaults.length),
  }))
}

function isBlockedOutlineOption(section: ResolvedOutlineOption): boolean {
  const key = section.key.trim().toLowerCase()
  const label = section.label.trim()
  return key === 'site_list' || ['网站列表', '官网列表', '网址列表'].includes(label)
}

function sanitizeOutlineKeys(keys: string[] | undefined): string[] | undefined {
  if (!keys) {
    return undefined
  }
  const blockedKeys = new Set(
    baseOutlineOptions.value.filter((section) => isBlockedOutlineOption(section)).map((section) => section.key),
  )
  const filtered = keys.filter((key) => !blockedKeys.has(key) && key.trim().toLowerCase() !== 'site_list')
  return filtered.length > 0 ? filtered : []
}

function buildFallbackTitles(
  _templateKey: string | undefined,
  currentBrand: string,
  primaryKeyword: string | undefined,
  locale: string,
): string[] {
  const subject = primaryKeyword?.trim() || currentBrand || 'GEO'
  const isEnglish = locale === 'en-US'

  return isEnglish
    ? [`A practical guide to ${subject}`, `${subject}: the key points worth covering`]
    : [`${subject} 实用内容指南`, `${subject} 值得展开的核心看点`]
}

function buildDefaultOutlineOptions(_templateKey: string | undefined, locale: string): string[] {
  const isEnglish = locale === 'en-US'

  return isEnglish
    ? ['Intro', 'Core Content', 'Key Points', 'Conclusion']
    : ['引言', '核心内容', '文章关键要点', '结论']
}

function resolveTemplateText(
  template: string | undefined,
  context: Record<string, string>,
): string {
  const raw = (template || '').trim()
  if (!raw) {
    return ''
  }

  return raw.replace(templateTokenPattern, (_match, token: string) => context[token] || '').trim()
}

function asRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null
  }
  return value as Record<string, unknown>
}

function stringFromUnknown(value: unknown): string | undefined {
  return typeof value === 'string' ? value : undefined
}

function stringListFromUnknown(value: unknown): string[] | undefined {
  if (!Array.isArray(value)) {
    return undefined
  }
  const items = value.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean)
  return items.length > 0 ? items : undefined
}

function booleanFromUnknown(value: unknown): boolean | undefined {
  return typeof value === 'boolean' ? value : undefined
}

function numberFromUnknown(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}

function numberListFromUnknown(value: unknown): number[] | undefined {
  if (!Array.isArray(value)) {
    return undefined
  }
  const items = value
    .map((item) => (typeof item === 'number' && Number.isFinite(item) ? item : NaN))
    .filter((item) => Number.isFinite(item) && item > 0)
  return items.length > 0 ? items : undefined
}

function clampStep(value: number | undefined): number {
  if (typeof value !== 'number' || Number.isNaN(value)) {
    return 0
  }
  if (value < 0) {
    return 0
  }
  if (value > 2) {
    return 2
  }
  return value
}

function parseDraftCompetitors(value: unknown): DraftCompetitor[] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
    .map((item) => asRecord(item))
    .filter((item): item is Record<string, unknown> => Boolean(item))
    .map((item) => ({
      key: nextCompetitorKey(),
      libraryId: numberFromUnknown(item.library_id),
      name: stringFromUnknown(item.name) || '',
      website: stringFromUnknown(item.website) || '',
      description: stringFromUnknown(item.description) || '',
      saved: Boolean(item.saved),
    }))
}

function parseCustomOutlineSections(value: unknown): ResolvedOutlineOption[] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
    .map((item) => asRecord(item))
    .filter((item): item is Record<string, unknown> => Boolean(item))
    .map((item, index) => ({
      key: stringFromUnknown(item.key) || `custom-${index + 1}`,
      label: stringFromUnknown(item.label) || '',
      default: Boolean(item.default),
      custom: Boolean(item.custom),
    }))
    .filter((item) => item.label.trim() !== '')
}

function parseOutlineNodes(value: unknown): TemplateOutlineNode[] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
    .map((item) => parseOutlineNode(item))
    .filter((item): item is TemplateOutlineNode => item !== null)
}

function parseOutlineNode(value: unknown): TemplateOutlineNode | null {
  const record = asRecord(value)
  if (!record) {
    return null
  }

  const outline = stringFromUnknown(record.outline)?.trim()
  if (!outline) {
    return null
  }

  const children = parseOutlineNodes(record.children)
  return children.length > 0 ? { outline, children } : { outline }
}

function readStepConfig(value: unknown): TemplateStepConfig | undefined {
  const record = asRecord(value)
  if (!record) {
    return undefined
  }
  return {
    title: stringFromUnknown(record.title),
    description: stringFromUnknown(record.description),
  }
}

function readCardCopy(value: unknown): TemplateCardCopy | undefined {
  const record = asRecord(value)
  if (!record) {
    return undefined
  }
  return {
    title: stringFromUnknown(record.title),
    hint: stringFromUnknown(record.hint),
    visible: booleanFromUnknown(record.visible),
  }
}

function readReviewIntroHookConfig(value: unknown): TemplateReviewIntroHookConfig | undefined {
  const record = asRecord(value)
  if (!record) {
    return undefined
  }

  const options = Array.isArray(record.options)
    ? record.options
        .map((item, index) => ({ item: asRecord(item), index }))
        .filter((entry): entry is { item: Record<string, unknown>; index: number } =>
          Boolean(entry.item),
        )
        .map(({ item, index }) => ({
          key: stringFromUnknown(item.key) || `review-hook-${index + 1}`,
          label:
            stringFromUnknown(item.label) || stringFromUnknown(item.key) || `Hook ${index + 1}`,
          description: stringFromUnknown(item.description),
          default: Boolean(item.default),
        }))
    : []

  return {
    title: stringFromUnknown(record.title),
    hint: stringFromUnknown(record.hint),
    field_name: stringFromUnknown(record.field_name),
    required: booleanFromUnknown(record.required),
    options,
  }
}

function readDerivedInputs(value: unknown): Record<string, TemplateDerivedInputConfig> | undefined {
  const record = asRecord(value)
  if (!record) {
    return undefined
  }

  const entries: Array<[string, TemplateDerivedInputConfig]> = []
  for (const [key, raw] of Object.entries(record)) {
    const item = asRecord(raw)
    if (!item) {
      continue
    }
    entries.push([
      key,
      {
        source: stringFromUnknown(item.source),
        fallback_number:
          typeof item.fallback_number === 'number' ? item.fallback_number : undefined,
      },
    ])
  }

  if (entries.length === 0) {
    return undefined
  }

  return Object.fromEntries(entries)
}

function resolveDerivedInputValue(
  _fieldName: string,
  config: TemplateDerivedInputConfig | undefined,
): JsonValue | undefined {
  if (!config?.source) {
    return undefined
  }

  switch (config.source) {
    case 'brand_name':
      return normalizedBrandName.value || undefined
    case 'official_website':
      return officialWebsite.value.trim() || undefined
    case 'brand_summary':
      return brandSummary.value.trim() || undefined
    case 'primary_keyword':
      return primaryKeyword.value || undefined
    case 'primary_keyword_or_brand':
      return primaryKeyword.value || normalizedBrandName.value || undefined
    case 'competitor_count': {
      const competitorCount = buildCompetitorPayload().length
      if (competitorCount > 0) {
        return competitorCount
      }
      if (typeof config.fallback_number === 'number') {
        return config.fallback_number
      }
      return undefined
    }
    default:
      return undefined
  }
}

function reviewIntroHookToneClass(key: string): string {
  switch (key) {
    case 'question':
      return 'review-hook-option--question'
    case 'fact':
      return 'review-hook-option--fact'
    case 'quote':
      return 'review-hook-option--quote'
    case 'story':
      return 'review-hook-option--story'
    case 'feeling':
      return 'review-hook-option--feeling'
    default:
      return 'review-hook-option--default'
  }
}

function reviewIntroHookBadgeText(key: string): string {
  switch (key) {
    case 'question':
      return '问'
    case 'fact':
      return '据'
    case 'quote':
      return '引'
    case 'story':
      return '事'
    case 'feeling':
      return '感'
    default:
      return Array.from(key.trim())[0] || '引'
  }
}

function buildTemplateRenderContext(input: {
  locale: string
  brandName: string
  officialWebsite: string
  brandSummary: string
  primaryQuestion: string
  supplementalQuestions: string[]
  inputParams: Record<string, JsonValue>
  competitorCount: number
}): Record<string, string> {
  const primaryQuestion = input.primaryQuestion.trim()
  const supplementalQuestions = dedupeStrings(input.supplementalQuestions)
  const context: Record<string, string> = {
    locale: input.locale.trim(),
    current_year: String(new Date().getFullYear()),
    brand_name: input.brandName.trim(),
    official_website: input.officialWebsite.trim(),
    brand_summary: input.brandSummary.trim(),
    brand_question: primaryQuestion,
    primary_question: primaryQuestion,
    primary_keyword: primaryQuestion || input.brandName.trim(),
    supplemental_questions: supplementalQuestions.join(', '),
  }

  for (const [key, value] of Object.entries(input.inputParams)) {
    const text = templateValueToString(value)
    if (text) {
      context[key] = text
    }
  }

  if (!context.count && input.competitorCount > 0) {
    context.count = String(input.competitorCount)
  }
  if (context.count) {
    context.top_count = context.count
  }

  return Object.fromEntries(Object.entries(context).filter(([, value]) => value.trim() !== ''))
}

function templateValueToString(value: JsonValue | undefined): string {
  if (value === null || value === undefined) {
    return ''
  }
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
    return String(value).trim()
  }
  if (Array.isArray(value)) {
    return value
      .map((item) => templateValueToString(item))
      .filter(Boolean)
      .join(', ')
  }
  return Object.values(value)
    .map((item) => templateValueToString(item))
    .filter(Boolean)
    .join(', ')
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => {
    window.setTimeout(resolve, ms)
  })
}

const dragState = ref<{ id: string; type: 'branch' | 'leaf'; parentId: string | null } | null>(null)
const dragOverId = ref<string | null>(null)
const structureDragKey = ref<string | null>(null)
const structureDragOverKey = ref<string | null>(null)

function onOutlineDragStart(
  e: DragEvent,
  id: string,
  type: 'branch' | 'leaf',
  parentId: string | null,
) {
  if (e.dataTransfer) {
    e.dataTransfer.effectAllowed = 'move'
    e.dataTransfer.setData('text/plain', JSON.stringify({ id, type, parentId }))
    e.dataTransfer.setData('text/id', id)
    setTimeout(() => {
      dragState.value = { id, type, parentId }
    }, 0)
  }
}

function onOutlineDragOver(e: DragEvent, id: string, type: 'branch' | 'leaf') {
  e.preventDefault()
  if (e.dataTransfer) {
    e.dataTransfer.dropEffect = 'move'
  }
  if (dragState.value?.type === type && dragState.value?.id !== id) {
    dragOverId.value = id
  }
}

function onOutlineDragLeave(_e: DragEvent, id: string) {
  if (dragOverId.value === id) {
    dragOverId.value = null
  }
}

function onOutlineDrop(
  e: DragEvent,
  targetId: string,
  targetType: 'branch' | 'leaf',
  targetParentId: string | null,
) {
  dragOverId.value = null
  if (!e.dataTransfer) return
  const raw = e.dataTransfer.getData('text/plain')
  if (!raw) return

  try {
    const data = JSON.parse(raw)
    const sourceId = data.id
    const sourceType = data.type
    const sourceParentId = data.parentId

    if (sourceId === targetId) return

    if (sourceType === 'branch' && targetType === 'branch') {
      const sourceIndex = generatedOutline.value.findIndex(
        (n: OutlineDraftNode) => n.id === sourceId,
      )
      const targetIndex = generatedOutline.value.findIndex(
        (n: OutlineDraftNode) => n.id === targetId,
      )
      if (sourceIndex > -1 && targetIndex > -1) {
        generatedOutline.value = moveArrayItemBefore(
          generatedOutline.value,
          sourceIndex,
          targetIndex,
        )
      }
    } else if (sourceType === 'leaf' && targetType === 'leaf') {
      const sourceParent = generatedOutline.value.find(
        (n: OutlineDraftNode) => n.id === sourceParentId,
      )
      const targetParent = generatedOutline.value.find(
        (n: OutlineDraftNode) => n.id === targetParentId,
      )

      if (sourceParent && targetParent) {
        const sourceIndex = sourceParent.children.findIndex(
          (c: OutlineDraftNode) => c.id === sourceId,
        )
        const targetIndex = targetParent.children.findIndex(
          (c: OutlineDraftNode) => c.id === targetId,
        )

        if (sourceIndex > -1 && targetIndex > -1) {
          generatedOutline.value = generatedOutline.value.map((node) => {
            if (node.id !== sourceParentId && node.id !== targetParentId) {
              return node
            }

            if (sourceParentId === targetParentId && node.id === sourceParentId) {
              return {
                ...node,
                children: moveArrayItemBefore(node.children, sourceIndex, targetIndex),
              }
            }

            if (node.id === sourceParentId) {
              const nextChildren = [...node.children]
              nextChildren.splice(sourceIndex, 1)
              return {
                ...node,
                children: nextChildren,
              }
            }

            if (node.id === targetParentId) {
              const movedNode = sourceParent.children[sourceIndex]
              if (!movedNode) {
                return node
              }
              const nextChildren = [...node.children]
              nextChildren.splice(targetIndex, 0, movedNode)
              return {
                ...node,
                children: nextChildren,
              }
            }

            return node
          })
        }
      }
    }
  } catch (err) {
    // ignore
  }
  dragState.value = null
}

function onOutlineDragEnd() {
  dragState.value = null
  dragOverId.value = null
}

function moveArrayItemBefore<T>(items: T[], sourceIndex: number, targetIndex: number): T[] {
  if (
    sourceIndex < 0 ||
    targetIndex < 0 ||
    sourceIndex >= items.length ||
    targetIndex >= items.length ||
    sourceIndex === targetIndex
  ) {
    return items
  }

  const next = [...items]
  const [moved] = next.splice(sourceIndex, 1)
  if (moved === undefined) {
    return items
  }

  const insertIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
  next.splice(insertIndex, 0, moved)
  return next
}

function onStructureDragStart(event: DragEvent, key: string): void {
  if (!event.dataTransfer) {
    return
  }

  event.dataTransfer.effectAllowed = 'move'
  event.dataTransfer.setData('text/plain', key)
  window.setTimeout(() => {
    structureDragKey.value = key
  }, 0)
}

function onStructureDragOver(event: DragEvent, key: string): void {
  event.preventDefault()
  if (structureDragKey.value && structureDragKey.value !== key) {
    structureDragOverKey.value = key
  }
}

function onStructureDragLeave(key: string): void {
  if (structureDragOverKey.value === key) {
    structureDragOverKey.value = null
  }
}

function onStructureDrop(event: DragEvent, key: string): void {
  event.preventDefault()
  const sourceKey = structureDragKey.value || event.dataTransfer?.getData('text/plain')
  structureDragOverKey.value = null

  if (!sourceKey || sourceKey === key) {
    return
  }

  const sourceIndex = outlineOptionOrder.value.findIndex((item) => item === sourceKey)
  const targetIndex = outlineOptionOrder.value.findIndex((item) => item === key)
  if (sourceIndex < 0 || targetIndex < 0) {
    return
  }

  outlineOptionOrder.value = moveArrayItemBefore(outlineOptionOrder.value, sourceIndex, targetIndex)
}

function onStructureDragEnd(): void {
  structureDragKey.value = null
  structureDragOverKey.value = null
}
</script>

<template>
  <div class="wizard-page">
    <div v-if="isPageLoading" class="wizard-page__loading">
      <a-skeleton active :paragraph="{ rows: 12 }" />
    </div>

    <template v-else-if="templateDetail">
      <header class="wizard-page__header">
        <div class="header-left">
          <a-button
            type="text"
            shape="circle"
            :loading="savingDraft"
            @click="handleSaveDraftAndExit"
          >
            <template #icon><LeftOutlined /></template>
          </a-button>
          <div class="header-title-box">
            <h2 class="header-title">
              创建 "{{ templateDetail.template_name }}" 文章
              <span class="header-accent">{{ templateMeta.accent }}</span>
            </h2>
            <p class="header-eyebrow">{{ templateMeta.helper }}</p>
          </div>
        </div>
      </header>

      <main class="wizard-page__main">
        <div class="wizard-steps-container">
          <a-steps :current="currentStep" class="wizard-steps" size="small">
            <a-step :title="basicStepCopy.title || t('templates.wizard.steps.basic')" />
            <a-step :title="structureStepCopy.title || t('templates.wizard.steps.structure')" />
            <a-step :title="generateStepCopy.title || t('templates.wizard.steps.generate')" />
          </a-steps>
        </div>

        <section v-if="currentStep === 0" class="wizard-section">
          <div class="wizard-card">
            <div class="wizard-card__header">
              <div>
                <h3>{{ brandCardCopy.title || t('templates.wizard.sections.brandInfo') }}</h3>
                <p>{{ brandCardCopy.hint || t('templates.wizard.hints.brandFlow') }}</p>
              </div>
            </div>

            <div class="field-grid">
              <div class="field-item">
                <label class="required-asterisk">
                  {{ t('templates.wizard.sections.language') }}
                </label>
                <a-select
                  v-model:value="articleLocale"
                  :options="[
                    { label: t('templates.wizard.localeOptions.zh'), value: 'zh-CN' },
                    { label: t('templates.wizard.localeOptions.en'), value: 'en-US' },
                  ]"
                />
              </div>

              <div class="field-item">
                <label class="required-asterisk">
                  {{ t('templates.wizard.sections.brandName') }}
                </label>
                <div class="current-brand-field">
                  <strong>{{ normalizedBrandName || t('shell.currentCompanyPlaceholder') }}</strong>
                  <span v-if="selectedBrand?.website">{{ selectedBrand.website }}</span>
                </div>
              </div>

              <div class="field-item">
                <label>{{ t('templates.wizard.sections.website') }}</label>
                <a-input
                  v-model:value="officialWebsite"
                  :placeholder="t('templates.wizard.placeholders.website')"
                />
              </div>
            </div>

            <div v-if="showBrandSummaryBox" class="summary-box">
              <div class="summary-box__label">
                {{ t('templates.wizard.sections.brandSummary') }}
              </div>
              <p>
                {{
                  brandSummary ||
                  selectedBrand?.description ||
                  t('templates.wizard.hints.brandSummaryPlaceholder')
                }}
              </p>
            </div>
          </div>

          <div class="wizard-card">
            <div class="wizard-card__header">
              <div>
                <h3>
                  {{ questionCardCopy.title || t('templates.wizard.sections.brandQuestions') }}
                </h3>
                <p>
                  {{ questionCardCopy.hint || t('templates.wizard.hints.brandQuestions') }}
                </p>
              </div>
            </div>
            <div class="brand-question-panel">
              <div class="field-item">
                <label class="required-asterisk">
                  {{ t('templates.wizard.sections.primaryQuestion') }}
                </label>
                <a-select
                  v-model:value="primaryQuestionId"
                  allow-clear
                  show-search
                  :disabled="!selectedBrandId"
                  :loading="questionsQuery.isPending.value"
                  :not-found-content="
                    selectedBrandId
                      ? t('templates.wizard.hints.questionEmpty')
                      : t('templates.wizard.hints.selectBrandFirst')
                  "
                  :options="questionOptions"
                  :placeholder="t('templates.wizard.placeholders.primaryQuestion')"
                  option-filter-prop="label"
                  style="width: 100%"
                />
              </div>

              <div class="field-item">
                <div class="optional-field-label">
                  <label>{{ t('templates.wizard.sections.supplementalQuestions') }}</label>
                  <span>{{ supplementalQuestionIds.length }}/3</span>
                </div>
                <a-select
                  v-model:value="supplementalQuestionIds"
                  mode="multiple"
                  show-search
                  :disabled="!selectedBrandId || !primaryQuestionId"
                  :loading="questionsQuery.isPending.value"
                  :max-tag-count="3"
                  :not-found-content="t('templates.wizard.hints.questionEmpty')"
                  :options="supplementalQuestionOptions"
                  :placeholder="t('templates.wizard.placeholders.supplementalQuestions')"
                  option-filter-prop="label"
                  style="width: 100%"
                />
                <p class="field-helper">
                  {{ t('templates.wizard.hints.supplementalQuestions') }}
                </p>
              </div>
            </div>
          </div>

          <div v-if="showCompetitorsCard" class="wizard-card">
            <div class="wizard-card__header">
              <div>
                <h3>
                  {{ competitorsCardCopy.title || t('templates.wizard.sections.competitors') }}
                </h3>
                <p>{{ competitorsCardCopy.hint || t('templates.wizard.hints.competitors') }}</p>
              </div>
              <div class="competitor-header-actions">
                <a-button
                  class="competitor-ai-button"
                  type="primary"
                  :loading="assistBusy"
                  :disabled="!canManageCompetitors"
                  @click="handleAnalyze"
                >
                  <template #icon><RobotOutlined /></template>
                  {{ analyzeButtonLabel }}
                </a-button>
                <a-button
                  type="default"
                  :disabled="!canManageCompetitors"
                  @click="addCompetitorRow"
                >
                  <template #icon><PlusOutlined /></template>
                  {{ t('templates.wizard.actions.addCompetitor') }}
                </a-button>
              </div>
            </div>

            <div v-if="competitorDrafts.length > 0" class="competitor-table">
              <div class="competitor-table__head">
                <span>{{ t('templates.wizard.table.website') }}</span>
                <span>{{ t('templates.wizard.table.name') }}</span>
                <span>{{ t('templates.wizard.table.description') }}</span>
                <span>{{ t('templates.wizard.table.actions') }}</span>
              </div>

              <div v-for="item in competitorDrafts" :key="item.key" class="competitor-table__row">
                <a-input
                  v-model:value="item.website"
                  :placeholder="t('templates.wizard.placeholders.competitorWebsite')"
                />
                <a-input
                  v-model:value="item.name"
                  :placeholder="t('templates.wizard.placeholders.competitorName')"
                />
                <a-input
                  v-model:value="item.description"
                  :placeholder="t('templates.wizard.placeholders.competitorDescription')"
                />
                <div class="competitor-actions">
                  <a-tooltip
                    :title="
                      canSaveCompetitorsToLibrary
                        ? undefined
                        : t('templates.wizard.messages.selectBrandBeforeFavorite')
                    "
                  >
                    <span>
                      <a-button
                        type="link"
                        :disabled="!canSaveCompetitorsToLibrary"
                        :loading="competitorSavingKey === item.key"
                        @click="handleFavoriteCompetitor(item)"
                      >
                        <template v-if="canSaveCompetitorsToLibrary && item.saved" #icon>
                          <StarFilled />
                        </template>
                        {{
                          canSaveCompetitorsToLibrary && item.saved
                            ? t('templates.wizard.actions.unfavorite')
                            : t('templates.wizard.actions.favorite')
                        }}
                      </a-button>
                    </span>
                  </a-tooltip>
                  <a-button type="text" danger @click="removeCompetitorRow(item.key)">
                    <template #icon><DeleteOutlined /></template>
                  </a-button>
                </div>
              </div>
            </div>

            <a-empty v-else :description="t('templates.wizard.hints.competitorEmptyEditable')" />
          </div>
        </section>

        <section v-else-if="currentStep === 1" class="wizard-section wizard-section--structure">
          <div class="wizard-structure-main">
            <div class="wizard-card">
              <div class="wizard-card__header">
                <div>
                  <h3>{{ titleCardCopy.title || t('templates.wizard.sections.chooseTitle') }}</h3>
                  <p>{{ titleCardCopy.hint || t('templates.wizard.hints.titleSelection') }}</p>
                </div>
              </div>

              <a-radio-group v-model:value="selectedTitle" class="title-option-group">
                <label v-for="option in titleOptions" :key="option" class="title-option">
                  <a-radio :value="option">{{ option }}</a-radio>
                </label>
              </a-radio-group>

              <div class="field-item field-item--compact">
                <label>{{ t('templates.wizard.sections.customTitle') }}</label>
                <a-input
                  v-model:value="customTitle"
                  :placeholder="t('templates.wizard.placeholders.customTitle')"
                />
              </div>
            </div>

            <div v-if="reviewIntroHookOptions.length > 0" class="wizard-card">
              <div class="wizard-card__header">
                <div>
                  <h3>
                    {{
                      reviewIntroHookConfig?.title || t('templates.wizard.sections.reviewIntroHook')
                    }}
                  </h3>
                  <p>
                    {{ reviewIntroHookConfig?.hint || t('templates.wizard.hints.reviewIntroHook') }}
                  </p>
                </div>
              </div>

              <div class="review-hook-grid">
                <button
                  v-for="option in reviewIntroHookOptions"
                  :key="option.key"
                  type="button"
                  :class="[
                    'review-hook-option',
                    reviewIntroHookToneClass(option.key),
                    { 'review-hook-option--active': reviewIntroHook === option.key },
                  ]"
                  @click="reviewIntroHook = option.key"
                >
                  <span class="review-hook-option__badge">
                    {{ reviewIntroHookBadgeText(option.key) }}
                  </span>
                  <span class="review-hook-option__body">
                    <span class="review-hook-option__title">{{ option.label }}</span>
                    <span v-if="option.description" class="review-hook-option__description">
                      {{ option.description }}
                    </span>
                  </span>
                  <span class="review-hook-option__check">✓</span>
                </button>
              </div>
            </div>

            <div class="wizard-card">
              <div class="wizard-card__header">
                <div>
                  <h3>{{ outlineCardCopy.title || t('templates.wizard.sections.structure') }}</h3>
                  <p>{{ outlineCardCopy.hint || t('templates.wizard.hints.structure') }}</p>
                </div>
              </div>

              <div class="outline-grid">
                <div
                  v-for="section in outlineOptions"
                  :key="section.key"
                  :class="[
                    'outline-option',
                    {
                      'outline-option--active': outlineSections.includes(section.key),
                      'outline-option--dragging': structureDragKey === section.key,
                      'outline-option--drag-over': structureDragOverKey === section.key,
                    },
                  ]"
                  draggable="true"
                  @dragstart="onStructureDragStart($event, section.key)"
                  @dragover="onStructureDragOver($event, section.key)"
                  @dragleave="onStructureDragLeave(section.key)"
                  @drop="onStructureDrop($event, section.key)"
                  @dragend="onStructureDragEnd"
                >
                  <a-checkbox
                    :checked="outlineSections.includes(section.key)"
                    @change="toggleOutlineSection(section.key, $event.target.checked)"
                  >
                    {{ section.label }}
                  </a-checkbox>
                  <div class="outline-option__actions">
                    <a-button
                      type="text"
                      danger
                      size="small"
                      :title="t('common.delete')"
                      @click.stop="removeOutlineSection(section.key)"
                    >
                      <template #icon><DeleteOutlined /></template>
                    </a-button>
                    <span class="outline-option__handle">
                      <HolderOutlined />
                    </span>
                  </div>
                </div>
              </div>

              <div v-if="canAddCustomOutline" class="field-item field-item--compact">
                <label>{{ t('templates.wizard.sections.customOutline') }}</label>
                <div class="custom-outline-row">
                  <a-input
                    v-model:value="customOutlineInput"
                    :maxlength="customOutlineMaxLength"
                    show-count
                    :placeholder="
                      wizardConfig?.outline?.custom_placeholder ||
                      t('templates.wizard.placeholders.customOutline')
                    "
                    @pressEnter="addCustomOutlineSection"
                  />
                  <a-button @click="addCustomOutlineSection">
                    {{ t('templates.wizard.actions.addSection') }}
                  </a-button>
                </div>
              </div>
            </div>
          </div>

          <aside class="wizard-card preview-card">
            <div class="wizard-card__header">
              <div>
                <h3>{{ previewCardCopy.title || outlinePreviewTitle }}</h3>
                <p>{{ previewCardCopy.hint || outlinePreviewCaption }}</p>
              </div>
            </div>

            <div class="preview-shell">
              <div class="preview-shell__chrome">
                <span />
                <span />
                <span />
              </div>
              <div class="preview-shell__body">
                <h4>{{ previewTitle }}</h4>

                <div v-if="previewSections.length > 0" class="preview-sections">
                  <div
                    v-for="section in previewSections"
                    :key="section.key"
                    class="preview-section"
                  >
                    <div class="preview-section__title">{{ section.label }}</div>
                    <div class="preview-line preview-line--wide" />
                    <div class="preview-line" />
                    <div class="preview-line preview-line--short" />
                  </div>
                </div>
                <div v-else class="preview-empty">
                  {{ t('templates.wizard.hints.previewEmpty') }}
                </div>
              </div>
            </div>
          </aside>
        </section>

        <section v-else class="wizard-section wizard-section--structure">
          <div class="wizard-structure-main">
            <div class="wizard-card">
              <div class="wizard-card__header">
                <div>
                  <h3>{{ t('templates.wizard.sections.generatedOutline') }}</h3>
                  <p>{{ t('templates.wizard.hints.generatedOutline') }}</p>
                </div>
                <a-button type="default" @click="addOutlineRootNode">
                  <template #icon><PlusOutlined /></template>
                  {{ t('templates.wizard.actions.addOutlineNode') }}
                </a-button>
              </div>

              <div v-if="generatedOutline.length > 0" class="outline-editor">
                <div v-for="section in generatedOutline" :key="section.id" class="outline-branch">
                  <div
                    class="outline-branch__header"
                    :class="{
                      'is-dragging': dragState?.id === section.id,
                      'drag-over': dragOverId === section.id,
                    }"
                    @dragover="onOutlineDragOver($event, section.id, 'branch')"
                    @dragleave="onOutlineDragLeave($event, section.id)"
                    @drop="onOutlineDrop($event, section.id, 'branch', null)"
                  >
                    <span class="outline-branch__dot" />
                    <a-textarea
                      v-model:value="section.outline"
                      class="outline-input"
                      :data-outline-node-id="section.id"
                      :auto-size="{ minRows: 1 }"
                      :placeholder="t('templates.wizard.placeholders.outlineNode')"
                    />
                    <div class="outline-node-actions">
                      <a-button type="text" @click="addOutlineChildNode(section.id)">
                        <template #icon><PlusOutlined /></template>
                      </a-button>
                      <a-button type="text" danger @click="removeOutlineNode(section.id)">
                        <template #icon><DeleteOutlined /></template>
                      </a-button>
                      <span
                        class="outline-node-handle"
                        draggable="true"
                        @dragstart="onOutlineDragStart($event, section.id, 'branch', null)"
                        @dragend="onOutlineDragEnd"
                      >
                        <HolderOutlined />
                      </span>
                    </div>
                  </div>

                  <div class="outline-branch__children">
                    <div
                      v-for="child in section.children"
                      :key="child.id"
                      class="outline-leaf"
                      :class="{
                        'is-dragging': dragState?.id === child.id,
                        'drag-over': dragOverId === child.id,
                      }"
                      @dragover="onOutlineDragOver($event, child.id, 'leaf')"
                      @dragleave="onOutlineDragLeave($event, child.id)"
                      @drop="onOutlineDrop($event, child.id, 'leaf', section.id)"
                    >
                      <span class="outline-leaf__dot" />
                      <a-textarea
                        v-model:value="child.outline"
                        class="outline-input"
                        :data-outline-node-id="child.id"
                        :auto-size="{ minRows: 1 }"
                        :placeholder="t('templates.wizard.placeholders.outlineNode')"
                      />
                      <div class="outline-node-actions">
                        <a-button type="text" @click="addOutlineNodeAfter(child.id)">
                          <template #icon><PlusOutlined /></template>
                        </a-button>
                        <a-button type="text" danger @click="removeOutlineNode(child.id)">
                          <template #icon><DeleteOutlined /></template>
                        </a-button>
                        <span
                          class="outline-node-handle"
                          draggable="true"
                          @dragstart="onOutlineDragStart($event, child.id, 'leaf', section.id)"
                          @dragend="onOutlineDragEnd"
                        >
                          <HolderOutlined />
                        </span>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              <div v-else class="preview-empty">
                {{ t('templates.wizard.hints.outlineEmpty') }}
              </div>

              <div class="field-item field-item--compact">
                <label>{{ t('templates.wizard.sections.keyPoints') }}</label>
                <a-textarea
                  v-model:value="keyPoints"
                  :rows="5"
                  :placeholder="t('templates.wizard.placeholders.keyPoints')"
                />
              </div>

              <div class="field-item field-item--compact">
                <label>引用知识库</label>
                <KnowledgeGroupSelect
                  v-model="selectedKnowledgeGroupIds"
                  placeholder="可选，用于补充文章内容和事实背景"
                />
              </div>

              <a-alert type="info" show-icon class="review-alert" :message="reviewAlertMessage" />
            </div>
          </div>

          <aside class="wizard-card preview-card">
            <div class="wizard-card__header">
              <div>
                <h3>{{ t('templates.wizard.sections.outlinePreview') }}</h3>
                <p>{{ t('templates.wizard.hints.outlinePreview') }}</p>
              </div>
            </div>

            <div class="preview-shell">
              <div class="preview-shell__chrome">
                <span />
                <span />
                <span />
              </div>
              <div class="preview-shell__body">
                <h4>{{ previewTitle }}</h4>
                <p v-if="keyPoints.trim()" class="preview-lead">{{ keyPoints.trim() }}</p>

                <div v-if="generatedOutlineNodes.length > 0" class="preview-sections">
                  <div
                    v-for="section in generatedOutlineNodes"
                    :key="section.outline"
                    class="preview-section"
                  >
                    <div class="preview-section__title">{{ section.outline }}</div>
                    <div
                      v-for="child in section.children || []"
                      :key="`${section.outline}-${child.outline}`"
                      class="preview-subsection"
                    >
                      <div class="preview-subsection__title">{{ child.outline }}</div>
                      <div class="preview-line preview-line--wide" />
                      <div class="preview-line" />
                      <div class="preview-line preview-line--short" />
                    </div>
                    <template v-if="!section.children || section.children.length === 0">
                      <div class="preview-line preview-line--wide" />
                      <div class="preview-line" />
                      <div class="preview-line preview-line--short" />
                    </template>
                  </div>
                </div>
                <div v-else class="preview-empty">
                  {{ t('templates.wizard.hints.outlineEmpty') }}
                </div>
              </div>
            </div>
          </aside>
        </section>
      </main>

      <footer class="wizard-page__footer">
        <div class="footer-previous">
          <a-button v-if="currentStep === 0" class="footer-btn" disabled>
            {{ t('common.previous') }}
          </a-button>
          <a-button v-else class="footer-btn" @click="handlePrev">
            {{ t('common.previous') }}
          </a-button>
        </div>
        <div class="footer-actions">
          <a-button class="footer-btn" :loading="savingDraft" @click="handleSaveDraftAndExit">
            {{ t('common.cancel') }}
          </a-button>
          <a-button
            v-if="currentStep < 2"
            type="primary"
            class="footer-btn footer-btn--primary"
            :loading="assistBusy"
            @click="handleNext"
          >
            {{ t('common.next') }}
          </a-button>
          <a-button
            v-else
            type="primary"
            class="footer-btn footer-btn--primary"
            :loading="mutation.isPending.value"
            @click="handleGenerate"
          >
            {{ t('templates.wizard.actions.submit') }}
          </a-button>
        </div>
      </footer>
    </template>

    <AiWaitingModal
      :open="assistModalOpen"
      :title="assistHeading"
      :progress="assistProgress"
      :stage="assistStages[assistStageIndex]"
    />

    <a-modal
      :open="leaveConfirmOpen"
      :title="t('templates.wizard.leaveConfirm.title')"
      :closable="false"
      :mask-closable="false"
      :keyboard="false"
      width="460px"
      wrap-class-name="leave-confirm-modal-wrap"
    >
      <p class="leave-confirm-text">{{ t('templates.wizard.leaveConfirm.content') }}</p>
      <template #footer>
        <a-button :disabled="leaveConfirmBusy" @click="handleStayOnWizard">
          {{ t('templates.wizard.leaveConfirm.stay') }}
        </a-button>
        <a-button danger :disabled="leaveConfirmBusy" @click="handleDiscardAndLeave">
          {{ t('templates.wizard.leaveConfirm.discard') }}
        </a-button>
        <a-button type="primary" :loading="leaveConfirmBusy" @click="handleSaveDraftAndLeave">
          {{ t('templates.wizard.leaveConfirm.save') }}
        </a-button>
      </template>
    </a-modal>
  </div>
</template>

<style scoped>
.wizard-page {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 120px);
  background: #fff;
}

.leave-confirm-text {
  margin: 0;
  color: #52627a;
  line-height: 1.7;
}

:deep(.leave-confirm-modal-wrap) {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 32px 16px 16px;
  overflow: auto;
}

:deep(.leave-confirm-modal-wrap .ant-modal) {
  top: 0;
  width: min(460px, calc(100vw - 32px)) !important;
  margin: 0;
  padding-bottom: 0;
}

:deep(.leave-confirm-modal-wrap .ant-modal-footer) {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 12px;
}

:deep(.leave-confirm-modal-wrap .ant-modal-footer .ant-btn) {
  margin-inline-start: 0 !important;
}

.wizard-page__loading {
  padding: 40px;
}

.wizard-page__header {
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 32px;
  border-bottom: 1px solid #edf2f7;
  background: #fff;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 16px;
}

.header-title-box {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.header-title {
  margin: 0;
  color: #111827;
  font-size: 20px;
  font-weight: 800;
}

.header-accent {
  margin-left: 8px;
  color: #1677ff;
  font-size: 14px;
  font-weight: 500;
}

.header-eyebrow {
  margin: 0;
  color: #6d7c94;
  font-size: 13px;
}

.wizard-page__main {
  display: flex;
  flex-direction: column;
  margin: 0 auto;
  width: 100%;
  max-width: 1160px;
  padding: 24px 32px 64px;
}

.wizard-steps-container {
  padding: 0 0 24px;
  border-bottom: 1px solid #edf2f7;
}

.wizard-section {
  display: flex;
  flex-direction: column;
}

.wizard-section--structure {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 460px;
  gap: 48px;
  align-items: start;
}

.wizard-structure-main {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 20px;
}

.wizard-card {
  padding: 32px 0;
  border-bottom: 1px solid #edf2f7;
}

.wizard-section > .wizard-card:last-child,
.wizard-structure-main > .wizard-card:last-child {
  border-bottom: none;
}

.wizard-card__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}

.wizard-card__header h3 {
  margin: 0;
  color: #111827;
  font-size: 16px;
  font-weight: 800;
  display: flex;
  align-items: center;
}

.wizard-card__header h3::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 16px;
  border-radius: 2px;
  background: #111827;
  margin-right: 12px;
}

.wizard-card__header p {
  margin: 6px 0 0 16px;
  color: #667085;
  font-size: 13px;
  line-height: 1.7;
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}

.field-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.field-item--compact {
  margin-top: 20px;
}

.field-item > label {
  display: inline-flex;
  align-items: center;
  margin-bottom: 12px;
  color: #1f2937;
  font-size: 15px;
  font-weight: 700;
}

.field-item > label::after {
  content: '：';
}

.current-brand-field {
  display: flex;
  min-height: 32px;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  padding: 5px 11px;
  border: 1px solid #d9d9d9;
  border-radius: 6px;
  background: #fafcff;
}

.current-brand-field strong {
  overflow: hidden;
  color: #111827;
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.current-brand-field span {
  overflow: hidden;
  color: #667085;
  font-size: 12px;
  line-height: 16px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.optional-field-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.optional-field-label label {
  display: inline-flex;
  align-items: center;
  color: #1f2937;
  font-size: 15px;
  font-weight: 700;
}

.optional-field-label label::after {
  content: '：';
}

.optional-field-label span {
  color: #667085;
  font-size: 12px;
  font-weight: 700;
}

.field-helper {
  margin: 0;
  color: #667085;
  font-size: 12px;
  line-height: 1.6;
}

.required-asterisk::before {
  margin-right: 4px;
  color: #ff4d4f;
  content: '*';
}

.brand-question-panel {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 18px;
}

.summary-box {
  margin-top: 18px;
  padding: 16px 18px;
  border: 1px solid rgba(80, 126, 255, 0.12);
  border-radius: 18px;
  background: linear-gradient(135deg, rgba(238, 244, 255, 0.86), rgba(247, 249, 255, 0.96));
}

.summary-box__label {
  margin-bottom: 8px;
  color: #456191;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.summary-box p {
  margin: 0;
  color: #24324c;
  line-height: 1.7;
}

.competitor-header-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px;
}

.competitor-ai-button {
  border: none;
  border-radius: 8px;
  background: #1677ff;
  box-shadow: 0 4px 12px rgba(22, 119, 255, 0.16);
  font-weight: 700;
}

.competitor-ai-button:hover:not([disabled]) {
  background: #0958d9 !important;
  box-shadow: 0 6px 16px rgba(22, 119, 255, 0.22);
}

.competitor-ai-button[disabled],
.competitor-ai-button.ant-btn-loading {
  background: #f1f5f9 !important;
  border: 1px solid #e2e8f0 !important;
  color: #94a3b8 !important;
  box-shadow: none !important;
}

.competitor-table {
  border: 1px solid rgba(20, 44, 88, 0.08);
  border-radius: 20px;
  overflow: hidden;
}

.competitor-table__head,
.competitor-table__row {
  display: grid;
  grid-template-columns: 1.2fr 1fr 1.4fr 160px;
  gap: 12px;
  align-items: center;
}

.competitor-table__head {
  padding: 14px 16px;
  background: rgba(241, 245, 255, 0.92);
  color: #52647f;
  font-size: 13px;
  font-weight: 700;
}

.competitor-table__row {
  padding: 16px;
  border-top: 1px solid rgba(20, 44, 88, 0.06);
}

.competitor-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
}

.title-option-group {
  display: grid;
  gap: 12px;
}

.title-option {
  display: block;
  padding: 16px 18px;
  border: 1px solid rgba(20, 44, 88, 0.08);
  border-radius: 18px;
  background: #ffffff;
  transition:
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    transform 0.2s ease;
}

.title-option:hover {
  border-color: rgba(22, 119, 255, 0.3);
  box-shadow: 0 10px 24px rgba(22, 119, 255, 0.08);
  transform: translateY(-1px);
}

.review-hook-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.review-hook-option {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  padding: 16px 18px;
  border: 1px solid rgba(20, 44, 88, 0.08);
  border-radius: 18px;
  background: #ffffff;
  text-align: left;
  cursor: pointer;
  transition:
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    transform 0.2s ease;
}

.review-hook-option:hover {
  border-color: rgba(22, 119, 255, 0.24);
  box-shadow: 0 10px 24px rgba(22, 119, 255, 0.08);
  transform: translateY(-1px);
}

.review-hook-option--active {
  border-color: rgba(22, 119, 255, 0.32);
  background: rgba(240, 246, 255, 0.96);
  box-shadow: 0 12px 28px rgba(22, 119, 255, 0.1);
}

.review-hook-option__badge {
  display: inline-flex;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 700;
}

.review-hook-option__body {
  display: flex;
  min-width: 0;
  flex: 1;
  flex-direction: column;
  gap: 6px;
}

.review-hook-option__title {
  color: #1f2f4d;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.5;
}

.review-hook-option__description {
  color: #71809a;
  font-size: 13px;
  line-height: 1.6;
}

.review-hook-option__check {
  color: #1677ff;
  font-size: 16px;
  font-weight: 700;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.review-hook-option--active .review-hook-option__check {
  opacity: 1;
}

.review-hook-option--question .review-hook-option__badge {
  background: rgba(255, 163, 67, 0.16);
  color: #d67a00;
}

.review-hook-option--fact .review-hook-option__badge {
  background: rgba(34, 197, 94, 0.14);
  color: #1f8f4a;
}

.review-hook-option--quote .review-hook-option__badge {
  background: rgba(59, 130, 246, 0.14);
  color: #2667d8;
}

.review-hook-option--story .review-hook-option__badge {
  background: rgba(34, 197, 94, 0.12);
  color: #198754;
}

.review-hook-option--feeling .review-hook-option__badge {
  background: rgba(245, 158, 11, 0.14);
  color: #c77a00;
}

.review-hook-option--default .review-hook-option__badge {
  background: rgba(107, 114, 128, 0.14);
  color: #4b5563;
}

.outline-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.outline-option {
  display: flex;
  min-height: 64px;
  align-items: center;
  justify-content: space-between;
  margin-inline-start: 0 !important;
  padding: 14px 16px;
  border: 1px solid rgba(20, 44, 88, 0.08);
  border-radius: 18px;
  background: rgba(252, 253, 255, 0.96);
  cursor: grab;
  transition:
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    transform 0.2s ease;
}

.outline-option:hover {
  border-color: rgba(22, 119, 255, 0.24);
  transform: translateY(-1px);
}

.outline-option--active {
  border-color: rgba(22, 119, 255, 0.28);
  background: rgba(240, 246, 255, 0.96);
  box-shadow: 0 10px 26px rgba(22, 119, 255, 0.08);
}

.outline-option--dragging {
  opacity: 0.68;
}

.outline-option--drag-over {
  border-color: rgba(22, 119, 255, 0.42);
  box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}

.outline-option :deep(.ant-checkbox) {
  align-self: flex-start;
  margin-top: 3px;
}

.outline-option :deep(.ant-checkbox-wrapper) {
  display: flex;
  flex: 1;
}

.outline-option :deep(.ant-checkbox + span) {
  padding-inline-end: 0;
  color: #22304d;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.6;
}

.outline-option__handle {
  margin-left: 12px;
  color: rgba(71, 93, 131, 0.56);
  font-size: 16px;
  line-height: 1;
  cursor: grab;
}

.outline-option__actions {
  display: flex;
  align-items: center;
  margin-left: 12px;
}

.custom-outline-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 12px;
}

.outline-editor {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.outline-branch {
  position: relative;
  padding-left: 26px;
}

.outline-branch::before {
  position: absolute;
  top: 12px;
  bottom: 8px;
  left: 10px;
  border-left: 1px dashed rgba(123, 141, 176, 0.36);
  content: '';
}

.outline-branch__header,
.outline-leaf {
  position: relative;
  display: flex;
  gap: 12px;
  align-items: center;
}

.outline-branch__dot,
.outline-leaf__dot {
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: #d7dfea;
  box-shadow: 0 0 0 4px rgba(238, 243, 252, 0.9);
}

.outline-branch__children {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 14px;
  margin-left: 16px;
  padding-left: 18px;
}

.outline-leaf::before {
  position: absolute;
  top: 50%;
  left: -18px;
  width: 12px;
  border-top: 1px dashed rgba(123, 141, 176, 0.36);
  content: '';
}

.outline-node-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.2s ease,
    visibility 0.2s ease;
}

.outline-branch__header:hover .outline-node-actions,
.outline-leaf:hover .outline-node-actions,
.outline-branch__header:focus-within .outline-node-actions,
.outline-leaf:focus-within .outline-node-actions {
  opacity: 1;
  visibility: visible;
}

.outline-node-handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #9aa8bf;
  font-size: 16px;
  cursor: grab;
}

.outline-node-handle:active {
  cursor: grabbing;
}

.outline-input {
  flex: 1;
  min-width: 0;
  border-color: transparent !important;
  background-color: transparent !important;
  box-shadow: none !important;
  transition: all 0.2s;
  padding: 4px 8px;
  resize: none;
}

.outline-input:hover {
  background-color: rgba(0, 0, 0, 0.02) !important;
}

.outline-input:focus,
.outline-input:focus-within {
  border-color: #1677ff !important;
  background-color: #fff !important;
  box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1) !important;
}

.is-dragging {
  opacity: 0.4;
}

.drag-over {
  border-top: 2px solid #1677ff;
}

.preview-card {
  position: sticky;
  top: 32px;
  border-bottom: none;
}

.preview-shell {
  overflow: hidden;
  border: 1px solid rgba(20, 44, 88, 0.08);
  border-radius: 24px;
  background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}

.preview-shell__chrome {
  display: flex;
  gap: 8px;
  padding: 14px 16px;
  border-bottom: 1px solid rgba(20, 44, 88, 0.08);
  background: rgba(246, 249, 255, 0.92);
}

.preview-shell__chrome span {
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: rgba(79, 111, 164, 0.22);
}

.preview-shell__body {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 22px 22px 26px;
}

.preview-shell__body h4 {
  margin: 0;
  color: #132341;
  font-size: 28px;
  font-weight: 700;
  line-height: 1.35;
}

.preview-lead {
  margin: 0;
  color: #5a6982;
  font-size: 14px;
  line-height: 1.7;
}

.preview-sections {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.preview-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.preview-subsection {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-left: 12px;
}

.preview-section__title {
  color: #1b2944;
  font-size: 20px;
  font-weight: 700;
}

.preview-subsection__title {
  color: #33435f;
  font-size: 15px;
  font-weight: 600;
}

.preview-line {
  height: 10px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(224, 232, 247, 0.92), rgba(241, 245, 252, 0.78));
}

.preview-line--wide {
  width: 100%;
}

.preview-line--short {
  width: 62%;
}

.preview-empty {
  padding: 18px;
  border: 1px dashed rgba(105, 128, 167, 0.28);
  border-radius: 18px;
  color: #6f7e97;
  font-size: 14px;
  line-height: 1.7;
}

.review-alert {
  margin-top: 20px;
}

.wizard-page__footer {
  position: sticky;
  bottom: 0;
  z-index: 100;
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  border-top: 1px solid rgba(20, 44, 88, 0.08);
  backdrop-filter: blur(14px);
  background: rgba(255, 255, 255, 0.88);
}

.footer-previous {
  flex: 0 0 auto;
}

.footer-actions {
  display: flex;
  gap: 12px;
  flex: 0 0 auto;
}

.footer-btn.ant-btn {
  min-width: 104px;
  height: 40px;
  border-radius: 8px;
  padding: 0 20px;
  font-size: 16px;
  font-weight: 400;
}

.footer-btn--primary.ant-btn {
  min-width: 144px;
}

@media (max-width: 1024px) {
  .wizard-section--structure {
    grid-template-columns: 1fr;
  }

  .field-grid {
    grid-template-columns: 1fr;
  }

  .brand-question-panel {
    grid-template-columns: 1fr;
  }

  .competitor-table__head,
  .competitor-table__row {
    grid-template-columns: 1fr;
  }

  .competitor-actions {
    justify-content: flex-start;
  }

  .competitor-header-actions {
    justify-content: flex-start;
  }

  .outline-grid {
    grid-template-columns: 1fr;
  }

  .review-hook-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .preview-card {
    position: static;
  }
}

@media (max-width: 768px) {
  .wizard-page__main {
    margin: 16px;
  }

  .wizard-card {
    padding: 18px;
    border-radius: 22px;
  }

  .wizard-page__footer {
    flex-direction: row;
    gap: 12px;
    align-items: center;
    padding: 14px 16px;
  }

  .custom-outline-row {
    grid-template-columns: 1fr;
  }

  .review-hook-grid {
    grid-template-columns: 1fr;
  }

  .footer-actions {
    justify-content: flex-end;
  }
}

@media (max-width: 560px) {
  .wizard-page__footer {
    gap: 8px;
  }

  .footer-actions {
    gap: 8px;
  }

  .footer-btn.ant-btn {
    min-width: auto;
    padding: 0 14px;
    font-size: 14px;
  }

  .footer-btn--primary.ant-btn {
    min-width: auto;
  }
}
</style>
