<template>
  <slot></slot>
  <!-- Teleport 到 body：避免父級 transform/overflow 讓 position:fixed 參考錯容器而被裁切 -->
  <Teleport to="body">
    <div
      v-if="showAssistantFab"
      ref="fabRef"
      class="ai-assistant-fab"
      :class="{ 'is-loading': pageLoading, 'is-hidden': dialogVisible, 'is-dragging': fabDragging }"
      :style="fabPositionStyle"
      @pointerdown="onFabPointerDown"
    >
      <div class="fab-icon-wrap">
        <Icon v-if="!pageLoading" icon="ep:chat-dot-round" class="fab-icon" />
        <Icon v-else icon="ep:loading" class="fab-icon fab-loading" />
      </div>
      <span class="fab-text">AI 决策助手</span>
    </div>
  </Teleport>

  <AiImageChatDialog
    v-model="dialogVisible"
    :initial-payload="initialPayload"
  />
</template>

<script setup lang="ts">
import html2canvas from 'html2canvas'
import { Icon } from '@/components/Icon'
import AiImageChatDialog from './AiImageChatDialog.vue'
import {
  AI_ASSISTANT_KEY,
  AI_CHAT_MESSAGES_KEY,
  AI_ASSISTANT_REPORT_CONTEXT_KEY,
  type AiAssistantReportContext
} from './useAiAssistant'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
import { routeShouldShowAiAssistantFab } from './aiFabRoute'

defineOptions({ name: 'AiAssistantProvider' })

/**
 * html2canvas@1.x 无法解析 CSS Color 4/5 的 color()、lab()、oklch() 等，会在解析阶段抛错。
 * 在克隆 DOM 上用安全色覆盖（仅影响克隆树，不影响真实页面）。
 * 需处理截图根节点及其子树，以及 html/body 祖先链（CSS 变量常挂在根上）。
 */
function sanitizeClonedDomForHtml2Canvas(clonedDoc: Document, clonedRoot: HTMLElement) {
  const win = clonedDoc.defaultView
  if (!win) return

  const unsafe = (v: string) =>
    typeof v === 'string' && /\b(color|lab|lch|oklch|oklab|hwb)\(/i.test(v)

  const patchOne = (el: Element) => {
    try {
      if (el instanceof HTMLElement) {
        const cs = win.getComputedStyle(el)
        const keys = [
          'color',
          'background-color',
          'border-top-color',
          'border-right-color',
          'border-bottom-color',
          'border-left-color',
          'outline-color',
          'column-rule-color',
          'caret-color'
        ] as const
        for (const key of keys) {
          const val = cs.getPropertyValue(key)
          if (val && unsafe(val)) {
            el.style.setProperty(key, key === 'color' ? '#303133' : '#ebeef5', 'important')
          }
        }
        const bg = cs.getPropertyValue('background')
        if (bg && unsafe(bg)) {
          el.style.setProperty('background', 'none', 'important')
          el.style.setProperty('background-color', '#ffffff', 'important')
        }
        const bgImg = cs.getPropertyValue('background-image')
        if (bgImg && unsafe(bgImg)) {
          el.style.setProperty('background-image', 'none', 'important')
          const bgc = cs.getPropertyValue('background-color')
          if (!bgc || unsafe(bgc)) {
            el.style.setProperty('background-color', '#fafafa', 'important')
          }
        }
        const bs = cs.getPropertyValue('box-shadow')
        if (bs && unsafe(bs)) {
          el.style.setProperty('box-shadow', 'none', 'important')
        }
        const ts = cs.getPropertyValue('text-shadow')
        if (ts && unsafe(ts)) {
          el.style.setProperty('text-shadow', 'none', 'important')
        }
        const flt = cs.getPropertyValue('filter')
        if (flt && unsafe(flt)) {
          el.style.setProperty('filter', 'none', 'important')
        }
        // 内联 style 里含 color()/lab() 等时尝试替换，仍无法解析则去掉内联避免拖垮 html2canvas
        const inline = el.getAttribute('style')
        if (inline && unsafe(inline)) {
          let s = inline
            .replace(/\bcolor\([^)]*\)/gi, 'rgb(48,49,51)')
            .replace(/\b(?:lab|lch|oklch|oklab|hwb)\([^)]*\)/gi, 'rgb(200,200,200)')
          if (unsafe(s)) {
            el.removeAttribute('style')
          } else {
            el.setAttribute('style', s)
          }
        }
      } else if (el instanceof SVGElement) {
        const cs = win.getComputedStyle(el)
        ;['fill', 'stroke'].forEach((attr) => {
          const val = cs.getPropertyValue(attr) || el.getAttribute(attr) || ''
          if (unsafe(val)) {
            el.setAttribute(attr, attr === 'fill' ? '#409eff' : '#303133')
          }
        })
      }
    } catch {
      /* 单节点忽略 */
    }
  }

  let ancestor: HTMLElement | null = clonedRoot
  while (ancestor) {
    patchOne(ancestor)
    ancestor = ancestor.parentElement
  }

  const walk = (el: Element) => {
    patchOne(el)
    for (const child of Array.from(el.children)) {
      walk(child)
    }
  }
  walk(clonedRoot)

  // 覆盖 :root 上可能含 color() 的主题变量（html2canvas 会解析到并报错）
  if (clonedDoc.head && !clonedDoc.getElementById('html2canvas-el-vars-override')) {
    const inj = clonedDoc.createElement('style')
    inj.id = 'html2canvas-el-vars-override'
    inj.textContent = `
      :root, html, body {
        --el-color-primary: #409eff !important;
        --el-color-primary-light-3: #79bbff !important;
        --el-color-primary-light-5: #a0cfff !important;
        --el-color-primary-light-7: #c6e2ff !important;
        --el-color-primary-light-8: #d9ecff !important;
        --el-color-primary-light-9: #ecf5ff !important;
        --el-color-primary-dark-2: #337ecc !important;
        --el-color-success: #67c23a !important;
        --el-color-warning: #e6a23c !important;
        --el-color-danger: #f56c6c !important;
        --el-color-info: #909399 !important;
        --el-text-color-primary: #303133 !important;
        --el-text-color-regular: #606266 !important;
        --el-text-color-secondary: #909399 !important;
        --el-text-color-placeholder: #a8abb2 !important;
        --el-text-color-disabled: #c0c4cc !important;
        --el-border-color: #dcdfe6 !important;
        --el-border-color-light: #e4e7ed !important;
        --el-border-color-lighter: #ebeef5 !important;
        --el-border-color-extra-light: #f2f6fc !important;
        --el-fill-color: #f0f2f5 !important;
        --el-fill-color-light: #f5f7fa !important;
        --el-fill-color-lighter: #fafafa !important;
        --el-fill-color-blank: #ffffff !important;
        --el-bg-color: #ffffff !important;
        --el-bg-color-page: #f2f3f5 !important;
      }
    `
    clonedDoc.head.appendChild(inj)
  }
}

function runHtml2CanvasOnClone(clonedDoc: Document, clonedNode: HTMLElement, transparentPixel: string) {
  sanitizeClonedDomForHtml2Canvas(clonedDoc, clonedNode)
  const imgs = clonedNode.querySelectorAll('img')
  imgs.forEach((img) => {
    const src = img.src || img.getAttribute('src')
    if (!src || src.startsWith('data:') || src.startsWith('blob:')) return
    try {
      if (new URL(src, window.location.href).origin !== window.location.origin) {
        const w = img.width || img.offsetWidth || (img as HTMLImageElement).naturalWidth
        const h = img.height || img.offsetHeight || (img as HTMLImageElement).naturalHeight
        img.src = transparentPixel
        img.removeAttribute('crossorigin')
        if (w && h) {
          img.setAttribute('width', String(w))
          img.setAttribute('height', String(h))
        }
      }
    } catch {
      /* ignore */
    }
  })
}

/** 登录 / SSO / 社交登录页不显示 AI 悬浮入口（兼容子路径） */
function isHideAssistantFabPath(path: string): boolean {
  const p = path || ''
  if (p === '/login' || p === '/sso' || p === '/social-login') return true
  if (p.startsWith('/login/') || p.startsWith('/sso/')) return true
  if (p.startsWith('/social-login')) return true
  return false
}

/** 仅由业务页通过 setAiAssistantFabEnabled(true) 开启；默认关闭，避免全站都出现悬浮入口 */
const fabEnabled = ref(false)
function setAiAssistantFabEnabled(enabled: boolean) {
  fabEnabled.value = enabled
}

const route = useRoute()

/** 依當前路由同步浮層（後台動態路由 name 常與組件 defineOptions 不一致，僅靠業務頁 setFab 易失效） */
watch(
  () => route.fullPath,
  () => {
    if (isHideAssistantFabPath(route.path)) {
      fabEnabled.value = false
      return
    }
    fabEnabled.value = routeShouldShowAiAssistantFab(route)
  },
  { immediate: true }
)

const showAssistantFab = computed(
  () => !isHideAssistantFabPath(route.path) && fabEnabled.value
)

/** 悬浮按钮位置（可拖拽），持久化到 localStorage */
const FAB_POS_STORAGE = 'ai-assistant-fab-position'
const fabRef = ref<HTMLElement | null>(null)
const fabPos = ref<{ left: number; top: number } | null>(null)
const fabDragging = ref(false)
let fabDragSession: {
  startX: number
  startY: number
  origLeft: number
  origTop: number
  moved: boolean
} | null = null

const fabPositionStyle = computed(() => {
  if (!fabPos.value) return {}
  return {
    left: `${fabPos.value.left}px`,
    top: `${fabPos.value.top}px`,
    right: 'auto',
    bottom: 'auto'
  }
})

function clampFabToViewport() {
  const el = fabRef.value
  if (!el) return
  const pad = 8
  const w = el.offsetWidth || 200
  const h = el.offsetHeight || 48
  const vw = window.innerWidth
  const vh = window.innerHeight
  if (!fabPos.value) return
  let { left, top } = fabPos.value
  // 持久化坐标在视窗外（换屏、缩放、误拖）时恢复默认右下角
  if (left + w < pad || top + h < pad || left > vw - pad || top > vh - pad) {
    fabPos.value = null
    try {
      localStorage.removeItem(FAB_POS_STORAGE)
    } catch {
      /* ignore */
    }
    return
  }
  const nl = Math.max(pad, Math.min(left, vw - w - pad))
  const nt = Math.max(pad, Math.min(top, vh - h - pad))
  if (nl !== left || nt !== top) {
    fabPos.value = { left: nl, top: nt }
    try {
      localStorage.setItem(FAB_POS_STORAGE, JSON.stringify(fabPos.value))
    } catch {
      /* ignore */
    }
  }
}

function onFabResizeClamp() {
  clampFabToViewport()
}

onMounted(() => {
  try {
    const raw = localStorage.getItem(FAB_POS_STORAGE)
    if (raw) {
      const p = JSON.parse(raw) as { left: number; top: number }
      if (typeof p.left === 'number' && typeof p.top === 'number' && Number.isFinite(p.left) && Number.isFinite(p.top)) {
        fabPos.value = p
      }
    }
  } catch {
    /* ignore */
  }
  nextTick(() => {
    clampFabToViewport()
  })
  window.addEventListener('resize', onFabResizeClamp)
})

onUnmounted(() => {
  window.removeEventListener('resize', onFabResizeClamp)
})

function onFabPointerMove(e: PointerEvent) {
  if (!fabDragSession) return
  const dx = e.clientX - fabDragSession.startX
  const dy = e.clientY - fabDragSession.startY
  if (Math.abs(dx) > 4 || Math.abs(dy) > 4) fabDragSession.moved = true
  const el = fabRef.value
  if (!el) return
  let nl = fabDragSession.origLeft + dx
  let nt = fabDragSession.origTop + dy
  const pad = 8
  const w = el.offsetWidth
  const h = el.offsetHeight
  nl = Math.max(pad, Math.min(nl, window.innerWidth - w - pad))
  nt = Math.max(pad, Math.min(nt, window.innerHeight - h - pad))
  fabPos.value = { left: nl, top: nt }
}

function onFabPointerUp(e: PointerEvent) {
  if (!fabDragSession) return
  const moved = fabDragSession.moved
  fabDragSession = null
  fabDragging.value = false
  const el = fabRef.value
  if (el) {
    try {
      el.releasePointerCapture(e.pointerId)
    } catch {
      /* ignore */
    }
  }
  window.removeEventListener('pointermove', onFabPointerMove)
  window.removeEventListener('pointerup', onFabPointerUp)
  if (fabPos.value) {
    try {
      localStorage.setItem(FAB_POS_STORAGE, JSON.stringify(fabPos.value))
    } catch {
      /* ignore */
    }
  }
  if (!moved) {
    void onFabClick()
  }
}

function onFabPointerDown(e: PointerEvent) {
  if (e.button !== 0) return
  if (pageLoading.value) return
  const el = fabRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  fabDragSession = {
    startX: e.clientX,
    startY: e.clientY,
    origLeft: r.left,
    origTop: r.top,
    moved: false
  }
  fabDragging.value = true
  el.setPointerCapture(e.pointerId)
  window.addEventListener('pointermove', onFabPointerMove)
  window.addEventListener('pointerup', onFabPointerUp)
}

const dialogVisible = ref(false)
const initialPayload = ref<{
  file?: File | null
  query?: string
  autoSend?: boolean
  prompt?: string
  moduleName?: string
  moduleCode?: string
  pendingUserText?: string
} | null>(null)
const chatMessages = ref<import('./useAiAssistant').ChatMessage[]>([])
provide(AI_CHAT_MESSAGES_KEY, chatMessages)

/** 报告单据上下文：模块名、模块编码、截图文件、截图目标 */
const reportModuleName = ref<string | null>(null)
const reportModuleCode = ref<string | null>(null)
const reportScreenshotFile = ref<File | null>(null)
const reportScreenshotTarget = ref<string | HTMLElement | null>(null)

/** 保存时自动截取当前组件页面（Y 轴全部内容），供报告单据使用 */
async function captureScreenshotForReport(): Promise<File | null> {
  const targetRef = reportScreenshotTarget.value
  if (!targetRef) return null
  const target = typeof targetRef === 'string' ? document.querySelector(targetRef) : targetRef
  if (!target || !(target instanceof HTMLElement)) return null

  const TRANSPARENT_PIXEL =
    'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
  const opts = {
    useCORS: true,
    allowTaint: true,
    logging: false,
    ignoreElements: (node: Node) => {
      const n = node as HTMLElement
      return n.classList?.contains('ai-assistant-fab') === true
    },
    onclone: (clonedDoc: Document, clonedNode: HTMLElement) => {
      runHtml2CanvasOnClone(clonedDoc, clonedNode, TRANSPARENT_PIXEL)
    }
  }

  try {
    const w = target.scrollWidth || target.offsetWidth
    const h = target.scrollHeight || target.offsetHeight
    const maxDim = Math.max(w, h)
    const scale = maxDim > 8192 ? 1 : Math.min(2, window.devicePixelRatio || 2)
    const canvas = await html2canvas(target, { ...opts, scale })
    return new Promise((resolve) => {
      canvas.toBlob(
        (blob) => {
          if (!blob) {
            resolve(null)
            return
          }
          resolve(new File([blob], 'screenshot.png', { type: 'image/png' }))
        },
        'image/png',
        0.9
      )
    })
  } catch (e) {
    console.error('报告截图失败:', e)
    return null
  }
}

provide<AiAssistantReportContext>(AI_ASSISTANT_REPORT_CONTEXT_KEY, {
  moduleName: reportModuleName,
  moduleCode: reportModuleCode,
  screenshotFile: reportScreenshotFile,
  captureScreenshot: captureScreenshotForReport
})

const pageLoading = ref(false)
function setPageLoading(v: boolean) {
  pageLoading.value = v
}

/** 当前页面的 AI 任务提示词，由页面通过 setPagePrompt 设置 */
const pagePromptRef = ref<string | null>(null)
function setPagePrompt(p: string | null) {
  pagePromptRef.value = p
}

/** 当前页面的模块名称，由页面通过 setPageModuleName 设置，用于报告单据 */
function setPageModuleName(name: string | null) {
  reportModuleName.value = name
}

/** 当前页面的模块编码，由页面通过 setPageModuleCode 设置，用于报告单据 */
function setPageModuleCode(code: string | null) {
  reportModuleCode.value = code
}

/** 当前页面截图目标（选择器或元素），保存报告时自动截取该区域 Y 轴全部内容 */
function setScreenshotTarget(target: string | HTMLElement | null) {
  reportScreenshotTarget.value = target
}

/** 截图并打开对话框（供 AiAssistantMark 等调用） */
async function openWithScreenshot(
  target: string | HTMLElement,
  options?: { moduleName?: string; moduleCode?: string; prompt?: string; pendingUserText?: string }
) {
  if (pageLoading.value) return
  await doScreenshot(
    target,
    options?.prompt,
    options?.moduleName,
    options?.moduleCode,
    options?.pendingUserText
  )
}

/** 点击悬浮框：只打开对话框，不截图（仅 AI 分析入口需要截图） */
async function onFabClick() {
  if (pageLoading.value) return
  const prompt = pagePromptRef.value || undefined
  const moduleName = reportModuleName.value || undefined
  const moduleCode = reportModuleCode.value || undefined
  initialPayload.value = { prompt, autoSend: false, moduleName, moduleCode }
  reportScreenshotFile.value = null
  dialogVisible.value = true
}

async function doScreenshot(
  el: string | HTMLElement,
  prompt?: string,
  moduleName?: string,
  moduleCode?: string,
  pendingUserText?: string
) {
  const target = typeof el === 'string' ? document.querySelector(el) : el
  if (!target || !(target instanceof HTMLElement)) {
    ElMessage.warning('未找到截图区域')
    dialogVisible.value = true
    return
  }
  const payloadPrompt = prompt
  const payloadModuleName = moduleName
  const payloadModuleCode = moduleCode
  const payloadPendingUserText = pendingUserText

  const TRANSPARENT_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

  const opts = {
    useCORS: true,
    allowTaint: true,
    logging: false,
    ignoreElements: (node: Node) => {
      const n = node as HTMLElement
      return n.classList?.contains('ai-assistant-fab') === true
    },
    onclone: (clonedDoc: Document, clonedNode: HTMLElement) => {
      runHtml2CanvasOnClone(clonedDoc, clonedNode, TRANSPARENT_PIXEL)
    }
  }

  const tryCapture = async (scale: number = 1): Promise<void> => {
    const canvas = await html2canvas(target, { ...opts, scale })
    return new Promise((resolve, reject) => {
      canvas.toBlob(
        (blob) => {
          if (!blob) {
            reject(new Error('toBlob失败'))
            return
          }
          const file = new File([blob], 'screenshot.png', { type: 'image/png' })
          initialPayload.value = {
            file,
            autoSend: false,
            prompt: payloadPrompt,
            moduleName: payloadModuleName,
            moduleCode: payloadModuleCode,
            pendingUserText: payloadPendingUserText
          }
          reportScreenshotFile.value = file
          if (payloadModuleCode) reportModuleCode.value = payloadModuleCode
          dialogVisible.value = true
          resolve()
        },
        'image/png',
        0.9
      )
    })
  }

  try {
    // html2canvas 会截取完整可滚动内容，用 scroll 尺寸判断
    const w = target.scrollWidth || target.offsetWidth
    const h = target.scrollHeight || target.offsetHeight
    const maxDim = Math.max(w, h)
    // 浏览器 canvas 单边约 16384px，scale 2 时需 maxDim <= 8192
    const scale = maxDim > 8192 ? 1 : Math.min(2, window.devicePixelRatio || 2)
    await tryCapture(scale)
  } catch (e) {
    try {
      await tryCapture(1)
    } catch (e2) {
      console.error('截图失败:', e2)
      ElMessage.error('截图失败，已打开对话框')
      initialPayload.value = null
      dialogVisible.value = true
    }
  }
}

watch(dialogVisible, (v) => {
  if (!v) {
    initialPayload.value = null
    reportScreenshotFile.value = null
  }
})

provide(AI_ASSISTANT_KEY, {
  openWithScreenshot,
  setPageLoading,
  setPagePrompt,
  setPageModuleName,
  setPageModuleCode,
  setScreenshotTarget,
  setAiAssistantFabEnabled
})
</script>

<style scoped>
/* 参考 AIRBT.html：白底、蓝边、圆角药丸、图标蓝圈、hover 放大 */
.ai-assistant-fab {
  position: fixed;
  right: 24px;
  bottom: 85px;
  z-index: 10050;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 20px;
  background: #fff;
  border: 1px solid #bfdbfe;
  border-radius: 9999px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  cursor: grab;
  touch-action: none;
  user-select: none;
  transition: transform 0.3s, box-shadow 0.3s, opacity 0.3s;
}
.ai-assistant-fab.is-dragging {
  cursor: grabbing;
  transform: none;
  transition: box-shadow 0.2s, opacity 0.2s;
}
.ai-assistant-fab:hover:not(.is-loading):not(.is-hidden):not(.is-dragging) {
  transform: scale(1.05);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3);
}
/* 抽屉已打开时隐藏悬浮按钮，避免与侧栏内容重叠、重复入口 */
.ai-assistant-fab.is-hidden {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}
.ai-assistant-fab.is-loading {
  cursor: not-allowed;
  pointer-events: auto;
}
.fab-icon-wrap {
  width: 32px;
  height: 32px;
  background: #2563eb;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3);
}
.fab-icon {
  font-size: 16px;
}
.fab-loading {
  animation: fab-spin 0.8s linear infinite;
}
@keyframes fab-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
.fab-text {
  font-size: 14px;
  font-weight: 700;
  color: #2563eb;
  letter-spacing: 0.025em;
}
</style>
