<template>
  <!-- 登录页面 — 左右分栏布局，包含登录和简历上传流程 -->
  <div class="login-view">

    <!-- ==================== 登录阶段（step 1 & 2）==================== -->
    <template v-if="step === 1 || step === 2">
      <!-- 左侧视觉动画面板 -->
      <section class="login-visual" ref="loginVisualRef" aria-label="OfferPai AI job platform">
        <div class="ai-hub-stage" ref="stageRef">
          <!-- 标题 -->
          <div class="hero-title" aria-hidden="true">
            AI 驱动的-
            <span class="hero-title__second-line">一站式求职辅助平台</span>
          </div>
          <!-- Nova 机器人核心（眼睛跟踪鼠标） -->
          <div class="nova-core" ref="novaCoreRef" aria-hidden="true">
            <img class="nova-core-base" :src="novaCoreSrc" alt="" draggable="false" />
            <img class="nova-eye nova-eye-left" ref="novaEyeLeftRef" :src="novaEyeLeftSrc" alt="" draggable="false" />
            <img class="nova-eye nova-eye-right" ref="novaEyeRightRef" :src="novaEyeRightSrc" alt="" draggable="false" />
          </div>
        </div>
        <!-- 底部 logo -->
        <img class="login-jianshi-logo" :src="jianshiLogoSrc" alt="" aria-hidden="true" draggable="false" />
      </section>

      <!-- 右侧表单面板 -->
      <div class="login-view__right">
        <div class="login-view__logo-row login-view__logo-row--right">
          <div class="login-view__logo-box login-view__logo-box--green">O</div>
          <span class="login-view__logo-text login-view__logo-text--dark">Offer派</span>
        </div>

        <div class="login-view__form-wrap">
          <!-- 步骤一：输入手机号发送验证码 -->
          <template v-if="step === 1">
            <div class="login-view__heading">
              <h2 class="login-view__title">手机号登录/注册</h2>
              <p class="login-view__subtitle">未注册用户验证后将自动注册并登录</p>
            </div>
            <div class="login-view__phone-row">
              <div class="login-view__country-code">
                <span class="login-view__flag">🇨🇳</span>
                <span class="login-view__code-text">+86</span>
                <span class="login-view__code-arrow">▾</span>
              </div>
              <div class="login-view__separator"></div>
              <input v-model="phone" type="tel" class="login-view__phone-input" placeholder="请输入手机号码" maxlength="11" />
            </div>
            <button class="login-view__send-btn" :disabled="!canSend" @click="handleSendCode">发送验证码</button>
          </template>

          <!-- 步骤二：输入验证码登录 -->
          <template v-if="step === 2">
            <div class="login-view__heading">
              <h2 class="login-view__title">请输入验证码</h2>
              <p class="login-view__subtitle">短信验证码已发送至 +86{{ phone }} <span class="fw600 ml5 cursor-po" @click="handleChangePhone">修改手机号</span></p>
            </div>
            <div class="login-view__otp-wrap" @click="focusOtpInput">
              <input ref="otpInputRef" v-model="otpValue" type="text" inputmode="numeric" class="login-view__otp-hidden-input" maxlength="6" :disabled="isLoggingIn" @input="handleOtpInput" />
              <div v-for="(_, idx) in 6" :key="idx" class="login-view__otp-box" :class="{ 'login-view__otp-box--active': idx === otpValue.length && !isLoggingIn, 'login-view__otp-box--filled': idx < otpValue.length }">
                <span v-if="idx < otpValue.length" class="login-view__otp-digit">{{ otpValue[idx] }}</span>
                <span v-else-if="idx === otpValue.length && !isLoggingIn" class="login-view__otp-cursor"></span>
              </div>
            </div>
            <p v-if="smsStatusMsg" class="login-view__sms-status" :class="{ 'login-view__sms-status--error': smsStatusIsError }">{{ smsStatusMsg }}</p>
            <button class="login-view__status-btn" :disabled="isLoggingIn || countdown > 0" @click="handleResendCode">
              <template v-if="isLoggingIn"><span class="login-view__spinner"></span><span>登录中</span></template>
              <template v-else-if="countdown > 0">{{ countdown }} 秒后可继续发送</template>
              <template v-else>再次发送验证码</template>
            </button>
          </template>

          <!-- 协议勾选（步骤一二通用） -->
          <div class="login-view__agreement">
            <div class="login-view__checkbox" :class="{ 'login-view__checkbox--checked': agreedTerms }" @click="agreedTerms = !agreedTerms">
              <span v-if="agreedTerms" class="login-view__check-icon">✓</span>
            </div>
            <span class="login-view__agreement-text">登录即表示同意</span>
            <span class="login-view__agreement-link" @click="openAgreement('ae8065i3')">《用户协议》</span>
            <span class="login-view__agreement-text">与</span>
            <span class="login-view__agreement-link" @click="openAgreement('hf8375i8')">《隐私政策》</span>
          </div>
        </div>
      </div>
    </template>

    <!-- ==================== 简历上传阶段（step 3）==================== -->
    <template v-if="step === 3">
      <!-- 左侧品牌面板 — 职位卡片风格 -->
      <div class="login-view__left login-view__left--resume">
        <!-- 顶部 Logo -->
        <div class="login-view__resume-logo">
          <div class="login-view__logo-box login-view__logo-box--light">O</div>
          <span class="login-view__logo-text login-view__logo-text--white">Offer派</span>
        </div>
        <!-- 浮动职位卡片 -->
        <div class="login-view__job-cards">
          <div v-for="(job, idx) in jobCards" :key="idx" class="login-view__job-card" :style="job.style">
            <span class="login-view__job-card-icon" :style="{ background: job.iconBg, color: job.iconColor }">{{ job.icon }}</span>
            <div class="login-view__job-card-info">
              <span class="login-view__job-card-title">{{ job.title }}</span>
              <span class="login-view__job-card-company">{{ job.company }}</span>
            </div>
            <span class="login-view__job-card-salary">{{ job.salary }}</span>
          </div>
        </div>
        <!-- 标语 -->
        <div class="login-view__resume-slogan">
          <h1>校招批量海投</h1>
          <h1>年轻人有更多的面试机会</h1>
        </div>
      </div>

      <!-- 右侧简历上传面板 -->
      <div class="login-view__right">
        <div class="login-view__form-wrap login-view__form-wrap--resume">

          <!-- 状态1：上传区域 -->
          <template v-if="resumeState === 'upload'">
            <div class="login-view__heading">
              <h2 class="login-view__title">一切美好，从简历开始~</h2>
              <p class="login-view__subtitle">自动解析您的简历，为你匹配最合适的岗位</p>
            </div>
            <!-- 拖拽上传区域 -->
            <div class="login-view__upload-area" @dragover.prevent @drop.prevent="handleDrop" @click="triggerFileInput">
              <div class="login-view__upload-circle"></div>
              <p class="login-view__upload-text">拖拽简历到这里，或点击上传</p>
              <p class="login-view__upload-hint">支持 PDF / DOC / DOCX，不超过 20MB</p>
            </div>
            <!-- 隐藏的文件选择器 -->
            <input ref="fileInputRef" type="file" accept=".pdf,.doc,.docx" class="login-view__file-input" @change="handleFileChange" />
            <!-- 上传按钮 -->
            <button class="login-view__upload-btn" @click="triggerFileInput">+ 上传简历</button>
          </template>

          <!-- 状态2：解析中 -->
          <template v-if="resumeState === 'parsing'">
            <div class="login-view__parsing">
              <h2 class="login-view__parsing-title">{{ parsingText }}</h2>
              <p class="login-view__parsing-subtitle">自动解析您的简历，为你匹配最合适的岗位</p>
            </div>
          </template>

          <!-- 状态3：上传失败 -->
          <template v-if="resumeState === 'failed'">
            <div class="login-view__heading">
              <h2 class="login-view__title login-view__title--error">上传失败！</h2>
              <p class="login-view__subtitle">您的简历解析失败了，请重试一次吧~</p>
            </div>
            <button class="login-view__upload-btn" @click="handleRetryUpload">重新上传</button>
          </template>

        </div>
      </div>
    </template>

    <!-- 协议预览弹窗 -->
    <AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import { sendSmsCode, smsLogin } from '@/api/auth'
import { fetchProfile, syncProfileFromResume } from '@/api/profile'
import { uploadResume } from '@/utils/aiRequest'
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'

// 左侧动画面板图片资源
import novaCoreSrc from '@/assets/images/loging_nodes/nova_core_not_eyes.png'
import novaEyeLeftSrc from '@/assets/images/loging_nodes/nova_eye_left.png'
import novaEyeRightSrc from '@/assets/images/loging_nodes/nova_eye_right.png'
import jianshiLogoSrc from '@/assets/images/loging_nodes/logo_jianshixingqiu.png'
import bgSrc from '@/assets/images/loging_nodes/ai_hub_nodes_bj_figma.png'
// 节点批次图片
import node01 from '@/assets/images/loging_nodes/01_jobs_total_board.png'
import node02 from '@/assets/images/loging_nodes/02_jobs_reference_deck.png'
import node03 from '@/assets/images/loging_nodes/03_jobs_match_radar.png'
import node04 from '@/assets/images/loging_nodes/04_jobs_role_chips.png'
import node05 from '@/assets/images/loging_nodes/05_jobs_daily_update.png'
import node06 from '@/assets/images/loging_nodes/06_delivery_flow_status.png'
import node07 from '@/assets/images/loging_nodes/07_delivery_page_detect.png'
import node08 from '@/assets/images/loging_nodes/08_delivery_task_queue.png'
import node09 from '@/assets/images/loging_nodes/09_delivery_submit_check.png'
import node10 from '@/assets/images/loging_nodes/10_delivery_plugin_online.png'
import node11 from '@/assets/images/loging_nodes/11_mentor_shadow_panel.png'
import node12 from '@/assets/images/loging_nodes/12_mentor_resource_pool.png'
import node13 from '@/assets/images/loging_nodes/13_mentor_field_bars.png'
import node14 from '@/assets/images/loging_nodes/14_mentor_booking_slots.png'
import node15 from '@/assets/images/loging_nodes/15_mentor_review_chip.png'

const store = useStore()
const router = useRouter()
const route = useRoute()

// ==================== 左侧动画面板 ====================
/** 舞台引用 */
const stageRef = ref<HTMLDivElement | null>(null)
/** 视觉区域引用 */
const loginVisualRef = ref<HTMLElement | null>(null)
/** Nova 核心引用 */
const novaCoreRef = ref<HTMLDivElement | null>(null)
/** 左眼引用 */
const novaEyeLeftRef = ref<HTMLImageElement | null>(null)
/** 右眼引用 */
const novaEyeRightRef = ref<HTMLImageElement | null>(null)

/** 动画时间配置 */
const animTiming = { fadeMs: 1400, holdMs: 3900, stepMs: 5300, lifeMs: 6700 }
/** 波次定时器 */
let waveTimer: ReturnType<typeof setTimeout> | null = null
/** ResizeObserver 实例 */
let resizeObs: ResizeObserver | null = null
/** 眨眼定时器 */
let blinkTimer: ReturnType<typeof setTimeout> | null = null
/** 空闲注视定时器 */
let idleLookTimer: ReturnType<typeof setTimeout> | null = null
/** 指针事件引用（用于卸载时清理） */
let handlePointerMove: (e: PointerEvent) => void = () => {}
let handlePointerLeave: () => void = () => {}

/** 节点批次数据 */
const nodeBatches = [
  [
    { src: node01, x: 53, y: 358, w: 226, h: 134 },
    { src: node02, x: 418, y: 182, w: 236, h: 128 },
    { src: node03, x: 696, y: 397, w: 208, h: 136 },
    { src: node04, x: 263, y: 700, w: 264, h: 116 },
    { src: node05, x: 630, y: 655, w: 214, h: 76 }
  ],
  [
    { src: node06, x: 344, y: 168, w: 306, h: 126 },
    { src: node07, x: 39, y: 478, w: 226, h: 136 },
    { src: node08, x: 618, y: 638, w: 214, h: 166 },
    { src: node09, x: 274, y: 720, w: 240, h: 122 },
    { src: node10, x: 697, y: 443, w: 230, h: 76 }
  ],
  [
    { src: node11, x: 372, y: 191, w: 232, h: 124 },
    { src: node12, x: 45, y: 395, w: 244, h: 140 },
    { src: node13, x: 688, y: 370, w: 214, h: 134 },
    { src: node14, x: 400, y: 707, w: 256, h: 124 },
    { src: node15, x: 688, y: 581, w: 222, h: 84 }
  ]
]

// ==================== 页面步骤控制 ====================
/** 当前步骤：1=输入手机号 2=输入验证码 3=上传简历 */
const step = ref(1)

// ==================== 登录相关状态 ====================
/** 手机号 */
const phone = ref('')
/** 验证码6位值 */
const otpValue = ref('')
/** 是否已勾选协议 */
const agreedTerms = ref(false)
/** 倒计时秒数 */
const countdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
/** 是否正在调用登录接口 */
const isLoggingIn = ref(false)
/** 验证码发送状态提示文字 */
const smsStatusMsg = ref('')
/** 状态提示是否为错误样式 */
const smsStatusIsError = ref(false)
/** OTP 输入框引用 */
const otpInputRef = ref<HTMLInputElement | null>(null)
/** 协议弹窗状态 */
const showAgreementDialog = ref(false)
const agreementCode = ref('')
/** 登录成功后的跳转路径（缓存） */
let loginRedirect = '/'

/** 发送按钮是否可用 */
const canSend = computed(() => phone.value.length === 11 && agreedTerms.value)

// ==================== 简历上传相关状态 ====================
/** 简历上传子状态：upload=上传区域 parsing=解析中 failed=失败 */
const resumeState = ref<'upload' | 'parsing' | 'failed'>('upload')
/** 解析中显示的文字 */
const parsingText = ref('正在解析个人资料...')
/** 解析文字轮换定时器 */
let parsingTimer: ReturnType<typeof setInterval> | null = null
/** 文件选择器引用 */
const fileInputRef = ref<HTMLInputElement | null>(null)

/** 左侧职位卡片硬编码数据 */
const jobCards = [
  { icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '前端工程师', company: '网易', salary: '18-28K', style: 'top: 0.6rem; left: 0.4rem;' },
  { icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '运营专员', company: '阿里巴巴', salary: '15-20K', style: 'top: 0.6rem; right: 0.8rem;' },
  { icon: '百', iconBg: '#E8F0FF', iconColor: '#4A7FE5', title: '算法工程师', company: '百度', salary: '30-45K', style: 'top: 1.5rem; left: 0.2rem;' },
  { icon: '美', iconBg: '#FFF3E8', iconColor: '#E8953F', title: '商业分析实习生', company: '美团', salary: '15K', style: 'top: 1.8rem; left: 2.8rem;' },
  { icon: '小', iconBg: '#E8FFE8', iconColor: '#4CAF50', title: '硬件产品', company: '小米', salary: '18-24K', style: 'top: 2.5rem; left: 0.3rem;' },
  { icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '产品运营', company: '京东', salary: '18-25K', style: 'top: 2.8rem; right: 0.6rem;' },
  { icon: '字', iconBg: '#F0E8FF', iconColor: '#7C4DFF', title: '产品经理', company: '字节跳动', salary: '25-35K', style: 'top: 3.5rem; left: 1.2rem;' },
  { icon: '增', iconBg: '#E8FFF5', iconColor: '#12C7BE', title: '增长运营', company: '拼多多', salary: '20-28K', style: 'top: 3.5rem; right: 0.4rem;' },
  { icon: '滴', iconBg: '#FFF8E1', iconColor: '#FFC107', title: '后端开发', company: '滴滴', salary: '22-32K', style: 'top: 4.3rem; left: 0.5rem;' },
  { icon: '网', iconBg: '#FFE8E8', iconColor: '#E85635', title: '数据分析师', company: '腾讯', salary: '20-30K', style: 'top: 4.6rem; right: 0.8rem;' },
]

/** 解析中文字轮换列表 */
const parsingSteps = ['正在解析个人资料...', '正在解析教育背景...', '正在解析工作经历...', '正在解析技能特长...', '简历解析成功，即将跳转']

// ==================== 登录逻辑 ====================

/** 打开协议预览 */
function openAgreement(code: string) {
  agreementCode.value = code
  showAgreementDialog.value = true
}

/** 聚焦OTP隐藏输入框 */
function focusOtpInput() {
  otpInputRef.value?.focus()
}

/** 修改手机号 — 回到步骤一并清空所有状态 */
function handleChangePhone() {
  step.value = 1
  phone.value = ''
  otpValue.value = ''
  countdown.value = 0
  smsStatusMsg.value = ''
  smsStatusIsError.value = false
  isLoggingIn.value = false
  if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
}

/** 开始60秒倒计时 */
function startCountdown() {
  countdown.value = 60
  countdownTimer = setInterval(() => {
    countdown.value--
    if (countdown.value <= 0 && countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
  }, 1000)
}

/** 步骤一：发送验证码 */
async function handleSendCode() {
  if (!phone.value) { ElMessage.warning('请输入手机号'); return }
  if (!agreedTerms.value) { ElMessage.warning('请先同意用户协议与隐私政策'); return }
  try {
    const res = await sendSmsCode(phone.value)
    if (res.code === '0') {
      smsStatusMsg.value = '验证码发送成功，请输入'
      smsStatusIsError.value = false
      step.value = 2
      startCountdown()
      nextTick(() => focusOtpInput())
    } else if (res.msg && res.msg.includes('请勿重复发送')) {
      smsStatusMsg.value = '验证码还在有效期内，请输入'
      smsStatusIsError.value = false
      step.value = 2
      startCountdown()
      nextTick(() => focusOtpInput())
    } else {
      smsStatusMsg.value = res.msg || '验证码发送失败'
      smsStatusIsError.value = true
    }
  } catch (err: any) {
    const msg = err?.response?.data?.msg || '验证码发送失败'
    smsStatusMsg.value = msg
    smsStatusIsError.value = true
  }
}

/** OTP输入处理 */
function handleOtpInput() {
  otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
  if (otpValue.value.length === 6) { handleLogin() }
}

/** 步骤二：再次发送验证码 */
async function handleResendCode() {
  if (isLoggingIn.value || countdown.value > 0) return
  try {
    const res = await sendSmsCode(phone.value)
    if (res.code === '0') {
      smsStatusMsg.value = '验证码发送成功，请输入'
      smsStatusIsError.value = false
      otpValue.value = ''
      startCountdown()
      nextTick(() => focusOtpInput())
    } else if (res.msg && res.msg.includes('请勿重复发送')) {
      smsStatusMsg.value = '验证码还在有效期内，请输入'
      smsStatusIsError.value = false
    } else {
      smsStatusMsg.value = res.msg || '验证码发送失败'
      smsStatusIsError.value = true
    }
  } catch (err: any) {
    const msg = err?.response?.data?.msg || '验证码发送失败'
    smsStatusMsg.value = msg
    smsStatusIsError.value = true
  }
}

/** 调用登录接口 */
async function handleLogin() {
  isLoggingIn.value = true
  try {
    const res = await smsLogin(phone.value, otpValue.value)
    if (res.code !== '0') {
      ElMessage.error('登录失败请核对验证码')
      otpValue.value = ''
      isLoggingIn.value = false
      nextTick(() => focusOtpInput())
      return
    }

    // 登录成功
    ElMessage.success(`欢迎回来，${res.data?.nick || ''}`)
    if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
    countdown.value = 0
    store.commit('SET_AUTHENTICATED', true)

    // 缓存跳转路径
    loginRedirect = (route.query.redirect as string) || '/'

    // 检测是否需要上传简历
    try {
      const profileRes = await fetchProfile()
      if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
        // 无个人资料，进入上传简历步骤
        step.value = 3
        resumeState.value = 'upload'
        isLoggingIn.value = false
        return
      }
    } catch {
      // 接口异常不阻塞，直接跳转
    }

    // 有个人资料，直接跳转
    router.replace(loginRedirect)
  } catch {
    ElMessage.error('登录失败请核对验证码')
    otpValue.value = ''
    isLoggingIn.value = false
    nextTick(() => focusOtpInput())
  }
}

// ==================== 简历上传逻辑 ====================

/** 触发文件选择 */
function triggerFileInput() {
  fileInputRef.value?.click()
}

/** 文件选择器 change 事件 */
function handleFileChange(e: Event) {
  const input = e.target as HTMLInputElement
  if (input.files && input.files[0]) {
    startUpload(input.files[0])
  }
}

/** 拖放上传 */
function handleDrop(e: DragEvent) {
  const file = e.dataTransfer?.files[0]
  if (file) { startUpload(file) }
}

/** 重新上传 */
function handleRetryUpload() {
  resumeState.value = 'upload'
}

/** 开始上传并解析简历 */
async function startUpload(file: File) {
  // 校验文件类型和大小
  const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
  if (!validTypes.includes(file.type)) {
    ElMessage.warning('请上传 PDF / DOC / DOCX 格式文件')
    return
  }
  if (file.size > 20 * 1024 * 1024) {
    ElMessage.warning('文件大小不能超过 20MB')
    return
  }

  // 进入解析状态
  resumeState.value = 'parsing'
  startParsingAnimation()

  try {
    const res = await uploadResume(file)
    if (res.code === 0 && res.data?.resumeId) {
      // 上传成功，同步个人资料
      await syncProfileFromResume(String(res.data.resumeId))
      // 停止轮换动画，显示成功
      stopParsingAnimation()
      parsingText.value = '简历解析成功，即将跳转'
      // 等1秒后跳转
      setTimeout(() => {
        router.replace(loginRedirect)
      }, 1000)
    } else {
      // 解析失败
      stopParsingAnimation()
      resumeState.value = 'failed'
    }
  } catch {
    stopParsingAnimation()
    resumeState.value = 'failed'
  }
}

/** 开始解析文字轮换动画 */
function startParsingAnimation() {
  let index = 0
  parsingText.value = parsingSteps[0]
  parsingTimer = setInterval(() => {
    index++
    if (index < parsingSteps.length - 1) {
      // 前4个每2秒切换
      parsingText.value = parsingSteps[index]
    } else {
      // 到第4个（技能特长）后停止轮换，保持显示直到接口返回
      parsingText.value = parsingSteps[parsingSteps.length - 2]
      if (parsingTimer) { clearInterval(parsingTimer); parsingTimer = null }
    }
  }, 2000)
}

/** 停止解析文字轮换动画 */
function stopParsingAnimation() {
  if (parsingTimer) { clearInterval(parsingTimer); parsingTimer = null }
}

/** 组件卸载时清理定时器 */
onBeforeUnmount(() => {
  if (countdownTimer) { clearInterval(countdownTimer) }
  if (parsingTimer) { clearInterval(parsingTimer) }
  if (waveTimer) { clearTimeout(waveTimer); waveTimer = null }
  if (blinkTimer) { clearTimeout(blinkTimer); blinkTimer = null }
  if (idleLookTimer) { clearTimeout(idleLookTimer); idleLookTimer = null }
  if (resizeObs) { resizeObs.disconnect(); resizeObs = null }
  window.removeEventListener('pointermove', handlePointerMove)
  window.removeEventListener('pointerleave', handlePointerLeave)
  document.removeEventListener('visibilitychange', handleVisibilityChange)
})

// ==================== 左侧动画面板逻辑 ====================

/** 工具函数 — 随机数 */
function random(min: number, max: number) { return min + Math.random() * (max - min) }

/** 适配舞台缩放，使其充满视觉区域 */
function fitVisualStage() {
  const stage = stageRef.value
  const visual = loginVisualRef.value
  if (!stage || !visual) return
  const baseWidth = 1100
  const baseHeight = 900
  const rect = visual.getBoundingClientRect()
  const scale = Math.min(rect.width / baseWidth, rect.height / baseHeight)
  const offsetX = (rect.width - baseWidth * scale) / 2
  const offsetY = (rect.height - baseHeight * scale) / 2
  stage.style.setProperty('--stage-scale', scale.toFixed(5))
  stage.style.setProperty('--stage-x', `${offsetX.toFixed(2)}px`)
  stage.style.setProperty('--stage-y', `${offsetY.toFixed(2)}px`)
}

/** 创建一个浮动节点面板并播放动画 */
function createPanel(spec: { src: string; x: number; y: number; w: number; h: number }) {
  const stage = stageRef.value
  if (!stage || document.hidden) return
  const img = document.createElement('img')
  img.className = 'node'
  img.src = spec.src
  img.alt = ''
  img.draggable = false
  img.style.setProperty('--x', `${spec.x}px`)
  img.style.setProperty('--y', `${spec.y}px`)
  img.style.setProperty('--w', `${spec.w}px`)
  img.style.setProperty('--h', `${spec.h}px`)
  img.style.setProperty('--life-ms', `${animTiming.lifeMs}ms`)

  let fallbackTimer: ReturnType<typeof setTimeout> | null = null
  const removePanel = () => {
    if (fallbackTimer) { clearTimeout(fallbackTimer); fallbackTimer = null }
    img.remove()
  }
  img.addEventListener('animationend', removePanel, { once: true })
  stage.appendChild(img)
  fallbackTimer = setTimeout(removePanel, animTiming.lifeMs + 500)
  requestAnimationFrame(() => {
    if (img.isConnected && !document.hidden) img.classList.add('is-running')
  })
}

/** 移除所有活动节点 */
function removeActiveNodes() {
  stageRef.value?.querySelectorAll('.node').forEach(n => n.remove())
}

/** 开始批次节点循环动画 */
function startNodeBatches() {
  let batchIndex = 0
  function spawnAndSchedule() {
    if (!document.hidden) {
      nodeBatches[batchIndex].forEach(createPanel)
      batchIndex = (batchIndex + 1) % nodeBatches.length
    }
    if (waveTimer) clearTimeout(waveTimer)
    waveTimer = setTimeout(() => { waveTimer = null; if (!document.hidden) spawnAndSchedule() }, animTiming.stepMs)
  }
  spawnAndSchedule()
}

/** 眼睛跟踪指针逻辑 */
function setupNovaEyeTracking() {
  const core = novaCoreRef.value
  const leftEye = novaEyeLeftRef.value
  const rightEye = novaEyeRightRef.value
  if (!core || !leftEye || !rightEye) return

  const config = { maxH: 0.72, maxV: 0.27, deadZone: 0.025, trackSmooth: 0.14, idleSmooth: 0.055 }
  const current = { lx: 0, ly: 0, rx: 0, ry: 0 }
  const target = { lx: 0, ly: 0, rx: 0, ry: 0 }
  let animating = false
  let pointerInside = false
  let eyeSmoothing = config.idleSmooth
  let lastIdleAngle: number | null = null

  function calcOffset(px: number, py: number, eye: HTMLElement) {
    const eyeRect = eye.getBoundingClientRect()
    const coreRect = core!.getBoundingClientRect()
    const dx = px - (eyeRect.left + eyeRect.width / 2)
    const dy = py - (eyeRect.top + eyeRect.height / 2)
    const dist = Math.hypot(dx, dy)
    const refSize = Math.max(coreRect.width, coreRect.height) * 0.35
    const norm = refSize > 0 ? dist / refSize : 0
    if (norm < config.deadZone) return { x: 0, y: 0 }
    const angle = Math.atan2(dy, dx)
    const t = Math.min((norm - config.deadZone) / (1 - config.deadZone), 1)
    const factor = t * t * (3 - 2 * t)
    return { x: Math.cos(angle) * eye.offsetWidth * config.maxH * factor, y: Math.sin(angle) * eye.offsetHeight * config.maxV * factor }
  }

  function applyTransforms() {
    current.lx += (target.lx - current.lx) * eyeSmoothing
    current.ly += (target.ly - current.ly) * eyeSmoothing
    current.rx += (target.rx - current.rx) * eyeSmoothing
    current.ry += (target.ry - current.ry) * eyeSmoothing
    leftEye!.style.transform = `translate3d(${current.lx.toFixed(2)}px, ${current.ly.toFixed(2)}px, 0)`
    rightEye!.style.transform = `translate3d(${current.rx.toFixed(2)}px, ${current.ry.toFixed(2)}px, 0)`
    const delta = Math.abs(target.lx - current.lx) + Math.abs(target.ly - current.ly) + Math.abs(target.rx - current.rx) + Math.abs(target.ry - current.ry)
    if (delta > 0.01) { requestAnimationFrame(applyTransforms) } else { animating = false }
  }

  function setTargets(lx: number, ly: number, rx = lx, ry = ly) {
    target.lx = lx; target.ly = ly; target.rx = rx; target.ry = ry
    if (!animating) { animating = true; requestAnimationFrame(applyTransforms) }
  }

  function blink() {
    leftEye!.classList.add('is-blinking')
    rightEye!.classList.add('is-blinking')
    setTimeout(() => { leftEye!.classList.remove('is-blinking'); rightEye!.classList.remove('is-blinking') }, 200)
  }

  function setIdleLook() {
    eyeSmoothing = config.idleSmooth
    const maxX = Math.min(leftEye!.offsetWidth, rightEye!.offsetWidth) * 0.43
    const maxY = Math.min(leftEye!.offsetHeight, rightEye!.offsetHeight) * 0.18
    let angle = random(0, Math.PI * 2)
    if (lastIdleAngle !== null) {
      for (let i = 0; i < 8; i++) {
        const diff = Math.abs(Math.atan2(Math.sin(angle - lastIdleAngle), Math.cos(angle - lastIdleAngle)))
        if (diff > 0.72) break
        angle = random(0, Math.PI * 2)
      }
    }
    lastIdleAngle = angle
    const x = Math.cos(angle) * maxX * random(0.92, 1)
    const y = Math.sin(angle) * maxY * random(0.92, 1)
    setTargets(x, y)
  }

  function scheduleIdleLook(delay = random(900, 2300)) {
    if (idleLookTimer) clearTimeout(idleLookTimer)
    idleLookTimer = setTimeout(() => {
      idleLookTimer = null
      if (!pointerInside && !document.hidden) {
        setIdleLook()
        if (Math.random() < 0.34) blink()
        scheduleIdleLook(random(1400, 2200))
      }
    }, delay)
  }

  function scheduleBlink() {
    blinkTimer = setTimeout(() => { blink(); scheduleBlink() }, 2400 + Math.random() * 4600)
  }

  // 暴露给外部事件处理
  handlePointerMove = (e: PointerEvent) => {
    const visual = loginVisualRef.value
    if (!visual) return
    const rect = visual.getBoundingClientRect()
    const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom
    if (inside) {
      pointerInside = true
      eyeSmoothing = config.trackSmooth
      if (idleLookTimer) { clearTimeout(idleLookTimer); idleLookTimer = null }
      const lo = calcOffset(e.clientX, e.clientY, leftEye!)
      const ro = calcOffset(e.clientX, e.clientY, rightEye!)
      setTargets(lo.x, lo.y, ro.x, ro.y)
    } else if (pointerInside) {
      pointerInside = false
      eyeSmoothing = config.idleSmooth
      setTargets(0, 0)
      scheduleIdleLook(650)
    }
  }

  handlePointerLeave = () => {
    pointerInside = false
    eyeSmoothing = config.idleSmooth
    setTargets(0, 0)
    scheduleIdleLook(650)
  }

  window.addEventListener('pointermove', handlePointerMove, { passive: true } as any)
  window.addEventListener('pointerleave', handlePointerLeave, { passive: true } as any)
  scheduleBlink()
  scheduleIdleLook(850)
}

/** 页面可见性切换处理 */
function handleVisibilityChange() {
  if (document.hidden) {
    if (waveTimer) { clearTimeout(waveTimer); waveTimer = null }
    removeActiveNodes()
  } else {
    removeActiveNodes()
    startNodeBatches()
  }
}

/** 挂载时初始化动画 */
onMounted(() => {
  // 设置背景图
  if (stageRef.value) {
    stageRef.value.style.backgroundImage = `url(${bgSrc})`
  }
  fitVisualStage()
  // 监听容器尺寸变化
  if (loginVisualRef.value && 'ResizeObserver' in window) {
    resizeObs = new ResizeObserver(fitVisualStage)
    resizeObs.observe(loginVisualRef.value)
  }
  // 启动眼睛跟踪
  setupNovaEyeTracking()
  // 启动节点动画
  startNodeBatches()
  // 监听页面可见性
  document.addEventListener('visibilitychange', handleVisibilityChange)
})
</script>
