<template>
  <el-dialog :model-value="visible" width="420px" align-center :close-on-click-modal="true" @close="handleClose">
    <template #header>安全验证</template>
    <div class="p-4">
      <div class="mb-3 text-sm text-gray-600">
        请在画布中点击字符：
        <span class="font-semibold text-blue-600">{{ targetChar }}</span>
      </div>

      <div>
        <canvas ref="canvas" :width="canvasWidth" :height="canvasHeight"
          :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" class="border rounded-md" role="img"
          :aria-label="`验证码：请点击字符 ${targetChar}`" @click="onCanvasClick" @keydown="onKeyDown" tabindex="0"></canvas>

        <div class="mt-3 flex items-center gap-3">
          <el-button size="small" @click="refresh" type="primary">换一组</el-button>
          <span class="text-xs text-gray-500">提示：点击画布中与目标字符相同的文字完成验证。</span>
        </div>
      </div>
    </div>

    <!-- 无底部按钮：验证通过自动触发，取消通过点击空白处关闭 -->
  </el-dialog>
</template>

<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'

const props = defineProps({
  visible: { type: Boolean, default: false },
  // 可选：如果需要后端校验，父组件可以传入一个函数 (payload) => Promise<boolean>
  serverValidate: { type: Function, required: false },
  // 画布大小
  width: { type: Number, default: 320 },
  height: { type: Number, default: 160 },
})

const emit = defineEmits(['close', 'passed', 'verify'])

// canvas refs
const canvas = ref(null)
const ctx = ref(null)
const canvasWidth = ref(props.width)
const canvasHeight = ref(props.height)

// 验证数据
const charset = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
const charCount = 6 // 画布上绘制的字符数
const chars = ref([]) // 每个字符的元数据 {char,x,y,angle,fontSize,color}
const targetChar = ref('')
const passed = ref(false)
const validating = ref(false)

// accessibility: 记录上次点击位置用于键盘验证
const lastClick = ref({ x: -1, y: -1 })

// 缓存噪声数据以避免每次重绘都变化
const noiseEllipses = ref([]) // {x,y,rx,ry,rot,color,alpha}
const noiseLines = ref([])    // {p:[x1,y1,x2,y2,x3,y3,x4,y4], width, alpha}
const noiseDots = ref([])     // {x,y,r,alpha}

function rand(min, max) { return Math.random() * (max - min) + min }

function pickChars() {
  const arr = []
  // Ensure uniqueness on a single canvas when possible
  const pool = charset.split('')
  // shuffle pool for random sampling without replacement
  for (let i = pool.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const tmp = pool[i]
    pool[i] = pool[j]
    pool[j] = tmp
  }

  if (charCount <= pool.length) {
    // take first `charCount` unique characters
    for (let i = 0; i < charCount; i++) {
      const ch = pool[i]
      arr.push({
        char: ch,
        x: rand(20, canvasWidth.value - 20),
        y: rand(30, canvasHeight.value - 20),
        angle: rand(-30, 30),
        fontSize: Math.floor(rand(24, 40)),
        color: `hsl(${Math.floor(rand(0, 360))}, 70%, ${Math.floor(rand(30, 70))}%)`
      })
    }
  } else {
    // use all unique chars first, then fill the rest with random picks (duplicates allowed)
    for (let i = 0; i < pool.length; i++) {
      const ch = pool[i]
      arr.push({
        char: ch,
        x: rand(20, canvasWidth.value - 20),
        y: rand(30, canvasHeight.value - 20),
        angle: rand(-30, 30),
        fontSize: Math.floor(rand(24, 40)),
        color: `hsl(${Math.floor(rand(0, 360))}, 70%, ${Math.floor(rand(30, 70))}%)`
      })
    }
    // fill remaining slots
    for (let i = pool.length; i < charCount; i++) {
      const ch = charset.charAt(Math.floor(Math.random() * charset.length))
      arr.push({
        char: ch,
        x: rand(20, canvasWidth.value - 20),
        y: rand(30, canvasHeight.value - 20),
        angle: rand(-30, 30),
        fontSize: Math.floor(rand(24, 40)),
        color: `hsl(${Math.floor(rand(0, 360))}, 70%, ${Math.floor(rand(30, 70))}%)`
      })
    }
  }

  chars.value = arr
  // choose a random target from the drawn characters
  targetChar.value = arr[Math.floor(Math.random() * arr.length)].char
  passed.value = false
}

function draw() {
  const c = canvas.value
  if (!c) return
  const $ctx = c.getContext('2d')
  ctx.value = $ctx
  // clear
  $ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)

  // background gradient
  const g = $ctx.createLinearGradient(0, 0, canvasWidth.value, canvasHeight.value)
  g.addColorStop(0, '#f6f8fa')
  g.addColorStop(1, '#e9eef6')
  $ctx.fillStyle = g
  $ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)

  // draw noisy shapes (ellipses)
  noiseEllipses.value.forEach((e) => {
    $ctx.beginPath()
    $ctx.fillStyle = `rgba(${e.color.r},${e.color.g},${e.color.b},${e.alpha})`
    $ctx.ellipse(e.x, e.y, e.rx, e.ry, e.rot, 0, Math.PI * 2)
    $ctx.fill()
  })

  // draw lines
  noiseLines.value.forEach((l) => {
    $ctx.beginPath()
    $ctx.strokeStyle = `rgba(0,0,0,${l.alpha})`
    $ctx.lineWidth = l.width
    const p = l.p
    $ctx.moveTo(p[0], p[1])
    $ctx.bezierCurveTo(p[2], p[3], p[4], p[5], p[6], p[7])
    $ctx.stroke()
  })

  // draw characters
  chars.value.forEach((cMeta) => {
    $ctx.save()
    $ctx.translate(cMeta.x, cMeta.y)
    $ctx.rotate(cMeta.angle * Math.PI / 180)
    // text style with slight shadow
    $ctx.font = `${cMeta.fontSize}px "Helvetica Neue", Arial, sans-serif`
    $ctx.fillStyle = cMeta.color
    $ctx.textAlign = 'center'
    $ctx.textBaseline = 'middle'
    $ctx.shadowColor = 'rgba(0,0,0,0.15)'
    $ctx.shadowBlur = 2
    $ctx.fillText(cMeta.char, 0, 0)
    // add small stroke to increase difficulty for OCR
    $ctx.lineWidth = 0.8
    $ctx.strokeStyle = 'rgba(0,0,0,0.08)'
    $ctx.strokeText(cMeta.char, 0, 0)
    $ctx.restore()
  })

  // dots
  noiseDots.value.forEach((d) => {
    $ctx.beginPath()
    $ctx.fillStyle = `rgba(0,0,0,${d.alpha})`
    $ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2)
    $ctx.fill()
  })

  // highlight last click: solid blue small dot
  if (lastClick.value.x >= 0 && lastClick.value.y >= 0) {
    $ctx.beginPath()
    $ctx.fillStyle = 'rgba(59,130,246,0.95)' /* blue-500 */
    $ctx.arc(lastClick.value.x, lastClick.value.y, 5, 0, Math.PI * 2)
    $ctx.fill()
  }
}

function refresh() {
  pickChars()
  // reset last click
  lastClick.value = { x: -1, y: -1 }
  // regenerate noise once for this round
  noiseEllipses.value = Array.from({ length: 12 }).map(() => ({
    x: rand(0, canvasWidth.value),
    y: rand(0, canvasHeight.value),
    rx: rand(10, 40),
    ry: rand(6, 30),
    rot: rand(0, Math.PI),
    alpha: rand(0.04, 0.15),
    color: { r: Math.floor(rand(0, 255)), g: Math.floor(rand(0, 255)), b: Math.floor(rand(0, 255)) },
  }))
  noiseLines.value = Array.from({ length: 6 }).map(() => ({
    p: [
      rand(0, canvasWidth.value), rand(0, canvasHeight.value),
      rand(0, canvasWidth.value), rand(0, canvasHeight.value),
      rand(0, canvasWidth.value), rand(0, canvasHeight.value),
      rand(0, canvasWidth.value), rand(0, canvasHeight.value),
    ],
    width: rand(1, 3),
    alpha: rand(0.06, 0.18),
  }))
  noiseDots.value = Array.from({ length: 40 }).map(() => ({
    x: rand(0, canvasWidth.value),
    y: rand(0, canvasHeight.value),
    r: rand(0.5, 2.5),
    alpha: rand(0.06, 0.25),
  }))
  // redraw slightly later to ensure font metrics
  requestAnimationFrame(draw)
}

function getClickChar(x, y) {
  // iterate characters from top-most (last) to first to mimic z-order
  for (let i = chars.value.length - 1; i >= 0; i--) {
    const c = chars.value[i]
    // transform point into rotated char space
    const dx = x - c.x
    const dy = y - c.y
    const rad = -c.angle * Math.PI / 180
    const tx = dx * Math.cos(rad) - dy * Math.sin(rad)
    const ty = dx * Math.sin(rad) + dy * Math.cos(rad)
    // enlarge hitbox a bit to improve click reliability
    const w = c.fontSize * 1.0
    const h = c.fontSize * 1.4
    if (tx >= -w && tx <= w && ty >= -h && ty <= h) return { char: c.char, index: i }
  }
  return null
}

// handle a hit result (reused by click and keyboard/forceValidate)
async function handleHit(hit) {
  if (!hit) return
  // check immediate match locally
  if (hit.char === targetChar.value) {
    // if serverValidate exists, rely on server result; otherwise pass locally
    if (props.serverValidate) {
      validating.value = true
      try {
        const ok = await props.serverValidate({ clicked: hit.char, target: targetChar.value })
        passed.value = !!ok
        emit('verify', { ok: !!ok })
        if (ok) {
          ElMessage.success('验证通过')
          emit('passed')
        }
      } catch (e) {
        passed.value = false
        emit('verify', { ok: false, error: e })
      } finally {
        validating.value = false
      }
    } else {
      passed.value = true
      emit('verify', { ok: true })
      ElMessage.success('验证通过')
      emit('passed')
    }
  } else {
    passed.value = false
    emit('verify', { ok: false })
  }
}

async function onCanvasClick(event) {
  // ignore clicks while a validation RPC is in flight
  if (validating.value) return

  const rect = canvas.value.getBoundingClientRect()
  // support non-square pixel ratio (safer)
  const scaleX = canvas.value.width / rect.width
  const scaleY = canvas.value.height / rect.height

  // for mouse events prefer clientX/clientY; if not present fallback to offset
  const clientX = typeof event.clientX === 'number' ? event.clientX : (event.offsetX + rect.left)
  const clientY = typeof event.clientY === 'number' ? event.clientY : (event.offsetY + rect.top)

  const x = (clientX - rect.left) * scaleX
  const y = (clientY - rect.top) * scaleY
  lastClick.value = { x, y }
  // redraw to show click marker without changing background
  draw()

  const hit = getClickChar(x, y)
  await handleHit(hit)
}

function forceValidate() {
  // use stored internal canvas coordinates directly (avoid reconstructing client coords)
  if (lastClick.value.x >= 0 && lastClick.value.y >= 0) {
    const hit = getClickChar(lastClick.value.x, lastClick.value.y)
    // reuse the same async flow
    handleHit(hit)
  }
}

function onKeyDown(e) {
  // allow Enter to force validate
  if (e.key === 'Enter') {
    forceValidate()
  } else if (e.key === 'r' || e.key === 'R') {
    refresh()
  }
}

function handleClose() {
  emit('close')
}

watch(() => props.visible, (v) => { if (v) refresh() })

onMounted(() => {
  // init
  if (canvas.value) ctx.value = canvas.value.getContext('2d')
  if (props.visible) refresh()
  window.addEventListener('resize', draw)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', draw)
})

// expose small API for parent via ref if needed
defineExpose({ refresh })
</script>

<style scoped>
canvas {
  display: block;
  outline: none
}
</style>
