<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">
                <vue-office-docx ref="docxViewer" v-if="docSrc" :src="docSrc" :request-options="docRequestOptions"
                    :options="docOptions" class="docx-viewer" @rendered="handleDocRendered" @error="handleDocError" />
            </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="!docSrc" 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 VueOfficeDocx from '@vue-office/docx'
import '@vue-office/docx/lib/index.css'

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

export default {
    name: 'DocumentSearchWord',
    components: {
        VueOfficeDocx,
    },
    data() {
        return {
            docUrl: '',
            docSrc: '',
            docOptions: {
                inWrapper: true,
            },
            docRequestOptions: {
                method: 'GET',
                headers: {},
                credentials: 'include',
            },
            keyword: '',
            loading: false,
            error: null,
            searchResults: [],
            currentResultIndex: -1,
            searching: false,
            searchSegments: [],
            docRendered: false,
        }
    },
    mounted() {
        this.docUrl = this.$route.query.url || DEFAULT_DOC_URL
        if (this.docUrl) {
            this.prepareDocumentSource()
        }
    },
    watch: {
        '$route.query.url'(newUrl, oldUrl) {
            if (newUrl !== oldUrl) {
                this.docUrl = newUrl || DEFAULT_DOC_URL
                this.resetViewerState()
                if (this.docUrl) {
                    this.prepareDocumentSource()
                }
            }
        },
    },
    methods: {
        prepareDocumentSource() {
            if (!this.docUrl) {
                this.docSrc = ''
                return
            }
            const headers = {}
            const token = this.$store?.getters?.token || window?.sessionStorage?.getItem('token')
            if (token) {
                headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`
            }
            this.docRequestOptions = {
                method: 'GET',
                headers,
                credentials: 'include',
            }
            this.loading = true
            this.error = null
            this.docRendered = false
            this.docSrc = this.docUrl
            this.prepareSearchSegments(true)
        },

        handleDocRendered() {
            this.loading = false
            this.error = null
            this.docRendered = true
            this.$nextTick(() => {
                this.prepareSearchSegments()
                if (this.keyword.trim()) {
                    this.highlightMatches()
                }
            })
        },

        handleDocError(err) {
            console.error('Word 文档预览失败:', err)
            this.loading = false
            this.error = err?.message || 'Word 文档加载失败，请稍后重试'
            this.docRendered = false
        },

        prepareSearchSegments(forceReset = false) {
            if (!this.docRendered) {
                if (forceReset) {
                    this.searchSegments = []
                }
                return
            }
            const wrapper = this.$refs.docWrapper
            if (!wrapper) return
            const root = wrapper.querySelector('.vue-office-docx-main') || wrapper
            if (!root) return

            const elements = Array.from(root.querySelectorAll('p, li, td, th, h1, h2, h3, h4, h5, h6, span'))
                .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.docSrc) 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.docSrc = ''
        },

        reload() {
            if (!this.docUrl) return
            this.prepareDocumentSource()
        },
    },
}
</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;
}

.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: #eaeaea;
    position: relative;
}

::v-deep .vue-office-docx {
    background: transparent !important;
    box-shadow: 0 10px 30px rgba(25, 64, 158, 0.12);
    border-radius: 8px;
    margin: 0 auto;
    max-width: 960px;
}

::v-deep .vue-office-docx-main {
    background: #ffffff;
    border-radius: 8px;
    overflow: hidden;
}

.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;
}

::v-deep .vue-office-docx table {
    border-collapse: collapse;
}

::v-deep .vue-office-docx table td,
::v-deep .vue-office-docx table th {
    border: 1px solid rgba(0, 0, 0, 0.12);
}

::v-deep .vue-office-docx .page {
    margin: 0 auto;
    box-shadow: none;
}

::v-deep .vue-office-docx .page-body {
    padding: 48px;
}
</style>