<template>
    <div class="document-search-word">
        <div class="search-toolbar">
            <el-input v-model="keyword" class="search-input" placeholder="请输入关键字" clearable
                @keyup.enter.native="handleSearch" @clear="resetSearch">
                <el-button slot="append" class="search-btn" icon="el-icon-search" @click="handleSearch"
                    :loading="searching" :disabled="searching">
                    <span v-if="!searching">搜索</span>
                    <span v-else>搜索中...</span>
                </el-button>
            </el-input>
            <div class="search-status" v-if="searchResults.length">
                <span class="result-indicator">{{ currentResultIndex + 1 }}/{{ searchResults.length }}</span>
                <el-button class="nav-btn" type="text" icon="el-icon-arrow-up" :disabled="!searchResults.length"
                    @click="goToPrevious">
                    上一处
                </el-button>
                <el-button class="nav-btn" type="text" icon="el-icon-arrow-down" :disabled="!searchResults.length"
                    @click="goToNext">
                    下一处
                </el-button>
            </div>
        </div>

        <div class="viewer-container">
            <div ref="docWrapper" class="doc-wrapper">
                <div ref="docxContainer" class="doc-content"></div>
            </div>

            <transition name="fade">
                <div v-if="loading" key="loading" class="overlay state-panel">
                    <i class="el-icon-loading state-icon"></i>
                    <p>正在加载 Word 文档，请稍候...</p>
                </div>
                <div v-else-if="error" key="error" class="overlay state-panel">
                    <i class="el-icon-warning-outline state-icon"></i>
                    <p>{{ error }}</p>
                    <el-button type="primary" @click="reload">重新加载</el-button>
                </div>
                <div v-else-if="!docUrl" key="no-url" class="overlay state-panel">
                    <i class="el-icon-document state-icon"></i>
                    <p>暂未指定 Word 文件，请通过路由参数 url 传入文件地址。</p>
                </div>
            </transition>
        </div>
    </div>
</template>

<script>
import * as docxPreview from 'docx-preview/dist/docx-preview.js'

const DEFAULT_DOC_URL = 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/11/11/887b35d28b2149b6a7555fb639be9411.docx'
// const DEFAULT_DOC_URL = 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/11/12/efec85cd3793469d9d0a6991d7d08f9b.doc'

export default {
    name: 'DocumentSearchWord',
    data() {
        return {
            docUrl: '',
            keyword: '',
            loading: false,
            error: null,
            searchResults: [],
            currentResultIndex: -1,
            searching: false,
            searchSegments: [],
            docRendered: false,
            abortController: null,
        }
    },
    mounted() {
        this.docUrl = this.$route.query.url || DEFAULT_DOC_URL
        if (this.docUrl) {
            this.loadDocument()
        }
    },
    beforeDestroy() {
        this.cancelFetch()
    },
    watch: {
        '$route.query.url'(newUrl, oldUrl) {
            if (newUrl !== oldUrl) {
                this.docUrl = newUrl || DEFAULT_DOC_URL
                this.resetViewerState()
                if (this.docUrl) {
                    this.loadDocument()
                }
            }
        },
    },
    methods: {
        cancelFetch() {
            if (this.abortController) {
                this.abortController.abort()
                this.abortController = null
            }
        },

        async loadDocument() {
            if (!this.docUrl) return
            this.loading = true
            this.error = null
            this.docRendered = false
            this.clearHighlights()
            this.searchSegments = []
            this.cancelFetch()
            const container = this.$refs.docxContainer
            if (container) {
                container.innerHTML = ''
            }

            try {
                const headers = {}
                const token = this.$store?.getters?.token || window?.sessionStorage?.getItem('token')
                if (token) {
                    headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`
                }
                this.abortController = new AbortController()
                const response = await fetch(this.docUrl, {
                    headers,
                    credentials: 'include',
                    signal: this.abortController.signal,
                })
                if (!response.ok) {
                    throw new Error(`加载 Word 文档失败：${response.statusText}`)
                }
                const arrayBuffer = await response.arrayBuffer()
                if (!container) {
                    throw new Error('未找到文档容器')
                }
                await docxPreview.renderAsync(arrayBuffer, container, null, {
                    className: 'docx-viewer',
                    inWrapper: true,
                    ignoreFonts: false,
                    breakPages: false,
                    ignoreWidth: false,
                    ignoreHeight: false,
                })
                this.wrapContentWithArticle(container)
                this.normalizeTableStyles(container)
                this.injectFontResolver(container)
                this.docRendered = true
                this.loading = false
                this.$nextTick(() => {
                    this.prepareSearchSegments()
                    if (this.keyword.trim()) {
                        this.highlightMatches()
                    }
                })
            } catch (error) {
                if (error.name === 'AbortError') return
                console.error('Word 文档预览失败:', error)
                this.loading = false
                this.error = error?.message || 'Word 文档加载失败，请稍后重试'
                this.docRendered = false
            } finally {
                this.abortController = null
            }
        },

        prepareSearchSegments(forceReset = false) {
            if (!this.docRendered) {
                if (forceReset) {
                    this.searchSegments = []
                }
                return
            }
            const container = this.$refs.docxContainer
            if (!container) return

            const selectors = [
                '.docx-viewer p',
                '.docx-viewer li',
                '.docx-viewer td',
                '.docx-viewer th',
                '.docx-viewer h1',
                '.docx-viewer h2',
                '.docx-viewer h3',
                '.docx-viewer h4',
                '.docx-viewer h5',
                '.docx-viewer h6',
                '.docx-viewer span',
            ]

            const elements = Array.from(container.querySelectorAll(selectors.join(',')))
                .filter((el) => (el.textContent || '').trim())

            elements.forEach((el, idx) => {
                if (typeof el.dataset.originalHtml === 'undefined') {
                    el.dataset.originalHtml = el.innerHTML
                }
                el.dataset.segmentIndex = idx
            })

            this.searchSegments = elements
        },

        async handleSearch() {
            const keyword = this.keyword.trim()
            if (!keyword) {
                this.resetSearch()
                return
            }
            if (!this.docRendered) {
                this.prepareSearchSegments()
            }
            this.searching = true
            await this.$nextTick()
            try {
                this.highlightMatches()
            } finally {
                this.searching = false
            }
        },

        highlightMatches() {
            const keyword = this.keyword.trim()
            if (!keyword) {
                this.resetSearch()
                return
            }
            if (!this.searchSegments.length) {
                this.prepareSearchSegments()
            }

            const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
            const pattern = new RegExp(`(${escapedKeyword})`, 'gi')

            this.clearHighlights()

            const results = []
            this.searchSegments.forEach((el) => {
                const originalHtml = el.dataset.originalHtml
                if (typeof originalHtml === 'undefined') return
                const text = el.textContent || ''
                pattern.lastIndex = 0
                if (!pattern.test(text)) {
                    return
                }
                pattern.lastIndex = 0
                const highlightedHtml = originalHtml.replace(pattern, '<mark class="search-highlight">$1</mark>')
                el.innerHTML = highlightedHtml
                const marks = el.querySelectorAll('mark.search-highlight')
                marks.forEach((mark) => {
                    results.push({ element: mark })
                })
            })

            this.searchResults = results
            if (!results.length) {
                this.currentResultIndex = -1
                this.$message && this.$message.info(`未找到“${keyword}”相关内容`)
                return
            }

            this.currentResultIndex = 0
            this.updateActiveHighlight()
            this.navigateToResult(this.currentResultIndex)
        },

        updateActiveHighlight() {
            this.searchResults.forEach((item, idx) => {
                if (!item.element) return
                if (idx === this.currentResultIndex) {
                    item.element.classList.add('is-active')
                } else {
                    item.element.classList.remove('is-active')
                }
            })
        },

        calculateOffsetTop(element, container) {
            let offset = 0
            let node = element
            while (node && node !== container) {
                offset += node.offsetTop || 0
                node = node.offsetParent
            }
            return offset
        },

        navigateToResult(index) {
            if (!this.searchResults.length || index < 0 || index >= this.searchResults.length) return
            this.currentResultIndex = index
            this.updateActiveHighlight()

            const target = this.searchResults[index]?.element
            const wrapper = this.$refs.docWrapper
            if (!target || !wrapper) return

            const absoluteTop = this.calculateOffsetTop(target, wrapper)
            const margin = 24
            wrapper.scrollTop = Math.max(absoluteTop - margin, 0)
        },

        async goToPrevious() {
            if (!this.searchResults.length) return
            let targetIndex = this.currentResultIndex
            if (targetIndex === -1) {
                targetIndex = this.searchResults.length - 1
            } else {
                targetIndex -= 1
                if (targetIndex < 0) {
                    targetIndex = this.searchResults.length - 1
                }
            }
            this.navigateToResult(targetIndex)
        },

        async goToNext() {
            if (!this.searchResults.length) return
            let targetIndex = this.currentResultIndex
            if (targetIndex === -1) {
                targetIndex = 0
            } else {
                targetIndex += 1
                if (targetIndex > this.searchResults.length - 1) {
                    targetIndex = 0
                }
            }
            this.navigateToResult(targetIndex)
        },

        clearHighlights() {
            if (!this.searchSegments.length) return
            this.searchSegments.forEach((el) => {
                if (typeof el.dataset.originalHtml !== 'undefined') {
                    el.innerHTML = el.dataset.originalHtml
                }
            })
        },

        resetSearch() {
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
        },

        resetViewerState() {
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
            this.searchSegments = []
            this.docRendered = false
            this.loading = !!this.docUrl
            this.error = null
            const container = this.$refs.docxContainer
            if (container) {
                container.innerHTML = ''
            }
        },

        reload() {
            if (!this.docUrl) return
            this.loadDocument()
        },

        injectFontResolver(container) {
            if (!container || container.querySelector('.docx-fonts-resolver')) return
            const resolver = document.createElement('div')
            resolver.className = 'docx-fonts-resolver'
            resolver.innerHTML = [
                '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '等线', 'Times New Roman', 'Arial'
            ].map(font => `<span style="font-family:${font};">${font}</span>`).join('')
            container.appendChild(resolver)
        },

        wrapContentWithArticle(container) {
            if (!container) return
            const sections = Array.from(container.querySelectorAll('.docx-wrapper section'))
            if (sections.length === 0) {
                this.wrapElementsInSections(container)
                return
            }
            sections.forEach((section) => {
                this.wrapElementsInSections(section)
            })
        },

        wrapElementsInSections(node) {
            if (!node) return

            // 获取所有需要包装的元素类型，但排除 header 和 footer 内的
            const selectors = [
                'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
                'ul', 'ol', 'blockquote', 'table'
            ]

            // 获取所有匹配的元素
            const allElements = Array.from(node.querySelectorAll(selectors.join(',')))

            // 过滤：只保留最外层的元素（没有父元素也是目标选择器）
            const elements = allElements.filter(el => {
                // 检查元素是否在 header 或 footer 内
                let parent = el.parentElement
                while (parent && parent !== node) {
                    const tag = parent.tagName ? parent.tagName.toLowerCase() : ''
                    if (tag === 'header' || tag === 'footer') {
                        return false // 在 header/footer 内，跳过
                    }

                    // 如果父元素也是目标选择器之一，说明当前元素是嵌套的，应该跳过
                    if (selectors.includes(tag)) {
                        return false
                    }

                    parent = parent.parentElement
                }
                return true
            })

            if (elements.length === 0) return

            // 按连续元素分组
            const elementGroups = this.groupConsecutiveElements(elements)

            // 为每个元素组创建 article 包装
            elementGroups.forEach(group => {
                if (group.length > 0) {
                    this.wrapElementGroupWithArticle(group)
                }
            })
        },

        groupConsecutiveElements(elements) {
            const groups = []
            let currentGroup = []

            elements.forEach((el, index) => {
                // 如果当前组为空，直接添加
                if (currentGroup.length === 0) {
                    currentGroup.push(el)
                    return
                }
                // 检查是否连续（在 DOM 中相邻）
                const lastEl = currentGroup[currentGroup.length - 1]
                let nextElement = lastEl.nextElementSibling

                // 跳过空白文本节点等，找到下一个元素
                while (nextElement && nextElement.nodeType !== 1) {
                    nextElement = nextElement.nextElementSibling
                }

                if (nextElement === el) {
                    // 连续的元素
                    currentGroup.push(el)
                } else {
                    // 不连续，开始新组
                    groups.push([...currentGroup])
                    currentGroup = [el]
                }

                // 如果是最后一个元素，结束当前组
                if (index === elements.length - 1) {
                    groups.push([...currentGroup])
                }
            })

            return groups
        },

        wrapElementGroupWithArticle(elements) {
            if (!elements || elements.length === 0) return

            const firstElement = elements[0]
            const parent = firstElement.parentElement

            // 创建 article 元素
            const article = document.createElement('article')
            article.className = 'docx-article-wrapper'

            // 在第一个元素前插入 article
            parent.insertBefore(article, firstElement)

            // 将所有元素移动到 article 中
            elements.forEach(el => {
                article.appendChild(el)
            })
        },

        normalizeTableStyles(container) {
            if (!container) return
            const tables = container.querySelectorAll('table')
            tables.forEach((table) => {
                table.style.width = 'auto'
                table.style.display = 'table'
                table.style.tableLayout = 'auto'
                table.style.textAlign = 'left'
                table.style.margin = '0 auto'
                const pTags = table.querySelectorAll('td p.docx-viewer.docx-viewer_TableParagraph')
                pTags.forEach((p) => {
                    p.style.lineHeight = '1.2'
                })
            })
        }
    },
}
</script>

<style scoped lang="scss">
.document-search-word {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 84px);
    background: linear-gradient(180deg, #f4f7ff 0%, #ffffff 100%);
    padding: 16px;
    box-sizing: border-box;
}

.document-search-word article.docx-article-wrapper {
    display: block;
    margin: 0 auto 32px;
    background: #ffffff;
    border-radius: 16px;
    box-shadow: 0 20px 48px rgba(25, 64, 158, 0.12);
    padding: 48px 56px;
    box-sizing: border-box;
    color: #1f2a62;
    line-height: 1.75;
    font-size: 14px;
}

.search-toolbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 16px;
    margin-bottom: 16px;
    flex-wrap: wrap;
}

.search-input {
    flex: 0 0 360px;
    width: 360px;
    max-width: 100%;
}

.search-status {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #506dff;
}

.result-indicator {
    font-weight: 600;
}

.nav-btn {
    padding: 0 8px;
}


.viewer-container {
    flex: 1;
    background: #ffffff;
    border-radius: 12px;
    overflow: hidden;
    position: relative;
    display: flex;
    min-height: 480px;
}

.doc-wrapper {
    flex: 1;
    overflow: auto;
    padding: 24px;
    background: #eef2ff;
    position: relative;
}

.doc-content {
    max-width: 960px;
    margin: 0 auto;
    background: #ffffff;
    border-radius: 16px;
    box-shadow: 0 18px 40px rgba(25, 64, 158, 0.12);
    padding: 48px 56px;
}

::v-deep .docx-wrapper {
    margin: 0 auto;
    background: linear-gradient(180deg, #fdfdff 0%, #f6f8ff 100%);
    border-radius: 16px;
    box-shadow: 0 24px 48px rgba(25, 64, 158, 0.16);
    overflow: hidden;
    border: 1px solid rgba(68, 112, 255, 0.18);
}

::v-deep .docx-wrapper section {
    background: transparent !important;
    padding: 48px 56px !important;
    line-height: 1.75;
    font-size: 14px;
    color: #1f2a62;
}


::v-deep .docx-wrapper h1,
::v-deep .docx-wrapper h2,
::v-deep .docx-wrapper h3,
::v-deep .docx-wrapper h4,
::v-deep .docx-wrapper h5,
::v-deep .docx-wrapper h6 {
    color: #1b2f72;
}

::v-deep .docx-wrapper .docx span {
    color: inherit;
}

.doc-content h1,
.doc-content h2,
.doc-content h3,
.doc-content h4,
.doc-content h5,
.doc-content h6 {
    font-weight: 700;
    margin: 24px 0 12px;
    color: #1b2f72;
}

.doc-content h1 {
    font-size: 28px;
}

.doc-content h2 {
    font-size: 24px;
}

.doc-content h3 {
    font-size: 20px;
}

.doc-content h4 {
    font-size: 18px;
}

.doc-content h5 {
    font-size: 16px;
}

.doc-content h6 {
    font-size: 15px;
}

.doc-content p {
    margin: 8px 0;
}

.doc-content ul,
.doc-content ol {
    padding-left: 28px;
    margin: 8px 0 12px;
}

.doc-content ul {
    list-style: disc;
}

.doc-content ol {
    list-style: decimal;
}

.doc-content blockquote {
    margin: 12px 0;
    padding: 12px 18px;
    background: rgba(68, 112, 255, 0.08);
    border-left: 4px solid rgba(68, 112, 255, 0.45);
    color: #3b4d86;
}

.doc-content table {
    width: 100%;
    border-collapse: collapse;
    margin: 16px 0;
    background: #ffffff;
    font-size: 13px;
}


.doc-content img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 12px auto;
}

.doc-content hr {
    border: none;
    border-top: 1px solid rgba(0, 0, 0, 0.1);
    margin: 24px 0;
}

.doc-content sup {
    font-size: 12px;
}

.overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(245, 247, 255, 0.92);
    z-index: 10;
    text-align: center;
}

.state-panel {
    flex-direction: column;
    gap: 12px;
    color: #6072a1;
}

.state-icon {
    font-size: 28px;
    color: #1f72ea;
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.25s ease;
}

.fade-enter,
.fade-leave-to {
    opacity: 0;
}

.search-highlight {
    background: rgba(255, 241, 168, 0.9);
    padding: 0 2px;
    border-radius: 2px;
}

.search-highlight.is-active {
    background: #206dff !important;
    color: #ffffff;
}
</style>