# -*- coding: utf-8 -*-
"""
日志检索 + 正文区（可嵌入标签页或独立窗口）。
"""

from __future__ import annotations

import re
from typing import List, Optional, Tuple

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor, QFont, QKeySequence, QTextCharFormat, QTextCursor, QShortcut
from PyQt6.QtWidgets import (
    QCheckBox,
    QDialog,
    QFrame,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPlainTextEdit,
    QPushButton,
    QScrollArea,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

from services.rg_search import compile_python_span_pattern, find_rg_executable, rg_filter_lines, rg_match_spans_qt


class LogViewerPane(QWidget):
    """检索行 + 只读日志正文，含 less 风格快捷键与高亮。"""

    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._full_text: str = ""
        self._current_match_range: Optional[Tuple[int, int]] = None
        self._spans_cache_key: Optional[Tuple] = None
        self._spans_cache_spans: List[Tuple[int, int]] = []
        self._build_ui()
        self._install_less_like_shortcuts()

    def _build_ui(self):
        layout = QVBoxLayout(self)
        layout.setSpacing(14)
        layout.setContentsMargins(0, 0, 0, 0)

        search_frame = QFrame()
        search_frame.setStyleSheet(
            "QFrame { background-color: #242932; border: 1px solid #3a4352; border-radius: 8px; }"
        )
        sf_outer = QVBoxLayout(search_frame)
        sf_outer.setSpacing(12)
        sf_outer.setContentsMargins(14, 14, 14, 14)

        row1 = QHBoxLayout()
        row1.setSpacing(10)
        row1.addWidget(self._pill_label("检索"))
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText(
            "关键字或正则 — Enter/F3 下一个 · Shift+F3 上一个 · 日志内 n/N、g/G 首尾"
        )
        self.search_input.setStyleSheet(self._input_style())
        self.search_input.setMinimumHeight(30)
        self.search_input.textChanged.connect(self._on_search_changed)
        self.search_input.returnPressed.connect(self.find_next)
        row1.addWidget(self.search_input, 1)
        sf_outer.addLayout(row1)

        row2 = QHBoxLayout()
        row2.setSpacing(16)
        self.case_cb = QCheckBox("区分大小写")
        self.case_cb.toggled.connect(self._on_search_changed)
        row2.addWidget(self.case_cb)

        self.filter_lines_cb = QCheckBox("仅显示匹配行")
        self.filter_lines_cb.toggled.connect(self._apply_filter_or_full)
        row2.addWidget(self.filter_lines_cb)

        self.regex_cb = QCheckBox("正则表达")
        self.regex_cb.setToolTip("勾选后关键字按正则解析；与「全词匹配」同时勾选时为 \\b(?:模式)\\b")
        self.regex_cb.toggled.connect(self._on_search_changed)
        row2.addWidget(self.regex_cb)

        self.whole_word_cb = QCheckBox("全词匹配")
        self.whole_word_cb.setToolTip("未勾选正则时：按单词边界匹配整词；勾选正则时：在模式外包裹单词边界")
        self.whole_word_cb.toggled.connect(self._on_search_changed)
        row2.addWidget(self.whole_word_cb)

        row2.addStretch()

        self.find_prev_btn = QPushButton("上一个 (N) ↑")
        self.find_next_btn = QPushButton("下一个 (n) ↓")
        for b in (self.find_prev_btn, self.find_next_btn):
            b.setCursor(Qt.CursorShape.PointingHandCursor)
            b.setStyleSheet(self._btn_style())
            b.setMinimumHeight(32)
        self.find_prev_btn.clicked.connect(self.find_prev)
        self.find_next_btn.clicked.connect(self.find_next)
        row2.addWidget(self.find_prev_btn)
        row2.addWidget(self.find_next_btn)
        sf_outer.addLayout(row2)

        self.rg_hint = QLabel("")
        self.rg_hint.setWordWrap(True)
        self._update_rg_hint()
        self.rg_hint.setStyleSheet("color: #aab4c3; font-size: 10px;")
        sf_outer.addWidget(self.rg_hint)

        layout.addWidget(search_frame)

        self.log_view = QPlainTextEdit()
        self.log_view.setReadOnly(True)
        self.log_view.setFont(QFont("Consolas", 10))
        if not QFont("Consolas").exactMatch():
            self.log_view.setFont(QFont("JetBrains Mono", 10))
        if not self.log_view.font().exactMatch():
            self.log_view.setFont(QFont("Courier New", 10))
        self.log_view.setStyleSheet(
            """
            QPlainTextEdit {
                background-color: #242932;
                color: #e7edf6;
                border: 1px solid #3a4352;
                border-radius: 8px;
                padding: 10px 12px;
                selection-background-color: #2b4d75;
            }
            """
        )
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setWidget(self.log_view)
        scroll.setMinimumHeight(220)
        scroll.setFrameShape(QFrame.Shape.NoFrame)
        layout.addWidget(scroll, 1)

    def _pill_label(self, text: str) -> QLabel:
        lb = QLabel(text)
        lb.setStyleSheet(
            """
            QLabel {
                color: #e8eef7;
                background-color: transparent;
                padding: 6px 8px;
                font-size: 11px;
                min-height: 22px;
            }
            """
        )
        return lb

    def _input_style(self) -> str:
        return (
            "QLineEdit { background-color: #242932; color: #e8eef7; "
            "border: 1px solid #3a4352; border-radius: 6px; padding: 7px 12px; min-height: 28px; }"
        )

    def _btn_style(self, bg: str = "#2b313c", hover: str = "#263a55") -> str:
        return (
            f"QPushButton {{ background-color: {bg}; color: #c6d7ef; border: 1px solid #4f6b92; "
            f"border-radius: 6px; padding: 8px 16px; font-weight: 600; min-height: 30px; }}"
            f"QPushButton:hover {{ background-color: {hover}; }}"
        )

    def _install_less_like_shortcuts(self):
        QShortcut(QKeySequence("n"), self.log_view).activated.connect(self.find_next)
        QShortcut(QKeySequence("Shift+N"), self.log_view).activated.connect(self.find_prev)
        QShortcut(QKeySequence("g"), self.log_view).activated.connect(self._go_to_file_start)
        QShortcut(QKeySequence("Shift+G"), self.log_view).activated.connect(self._go_to_file_end)
        QShortcut(QKeySequence("/"), self.log_view).activated.connect(self._focus_search_bar)
        for seq, slot in (
            ("F3", self.find_next),
            ("Shift+F3", self.find_prev),
            ("Ctrl+F", self._focus_search_bar),
        ):
            sc = QShortcut(QKeySequence(seq), self)
            sc.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
            sc.activated.connect(slot)

    def _focus_search_bar(self):
        self.search_input.setFocus(Qt.FocusReason.ShortcutFocusReason)
        self.search_input.selectAll()

    def _go_to_file_start(self):
        c = QTextCursor(self.log_view.document())
        c.movePosition(QTextCursor.MoveOperation.Start)
        self.log_view.setTextCursor(c)
        self.log_view.ensureCursorVisible()

    def _go_to_file_end(self):
        c = QTextCursor(self.log_view.document())
        c.movePosition(QTextCursor.MoveOperation.End)
        self.log_view.setTextCursor(c)
        self.log_view.ensureCursorVisible()

    def set_full_text(self, text: str, reset_search: bool = True):
        """设置正文并重置检索状态（新文件时 reset_search=True）。"""
        self._full_text = text
        self._current_match_range = None
        self._spans_cache_key = None
        if reset_search:
            self.search_input.blockSignals(True)
            self.search_input.clear()
            self.search_input.blockSignals(False)
            self.case_cb.setChecked(False)
            self.filter_lines_cb.setChecked(False)
            self.regex_cb.setChecked(False)
            self.whole_word_cb.setChecked(False)
        self._apply_filter_or_full()

    def _update_rg_hint(self):
        if find_rg_executable():
            self.rg_hint.setText(
                "检测到 rg：匹配行过滤、高亮与上一个/下一个均与 ripgrep 一致（Rust 正则）；"
                "未检测到 rg 时回退为 Python 正则。"
            )
        else:
            self.rg_hint.setText(
                "未检测到 rg.exe：匹配行过滤与主界面「日志查看 → 全局搜索」将回退为内置规则；"
                "可将 rg.exe 放在程序目录（与 Server Manager 同级）。"
            )

    def get_full_text(self) -> str:
        return self._full_text

    def _on_search_changed(self):
        self._current_match_range = None
        self._spans_cache_key = None
        self._apply_filter_or_full()

    def _apply_filter_or_full(self):
        if self.filter_lines_cb.isChecked():
            q = (self.search_input.text() or "").strip()
            if not q:
                display = self._full_text
            else:
                display = None
                if find_rg_executable():
                    filtered, err = rg_filter_lines(
                        self._full_text,
                        q,
                        self.case_cb.isChecked(),
                        self.regex_cb.isChecked(),
                        self.whole_word_cb.isChecked(),
                    )
                    if err is None and filtered is not None:
                        display = filtered
                if display is None:
                    pat = self._compile_pattern()
                    if pat is None:
                        display = self._full_text
                    else:
                        lines = self._full_text.splitlines()
                        display = "\n".join(line for line in lines if pat.search(line))
            self._set_plain(display)
        else:
            self._set_plain(self._full_text)

    def _set_plain(self, text: str):
        self.log_view.setPlainText(text)
        if not self.filter_lines_cb.isChecked():
            self._highlight_matches()

    def _search_needle(self) -> str:
        return self.search_input.text() or ""

    def _compile_pattern(self) -> Optional[re.Pattern]:
        """rg 不可用或失败时，用 Python re 过滤/高亮/跳转。"""
        return compile_python_span_pattern(
            self._search_needle(),
            self.case_cb.isChecked(),
            self.regex_cb.isChecked(),
            self.whole_word_cb.isChecked(),
        )

    def _match_spans_in_plain(self, plain: str) -> List[Tuple[int, int]]:
        """与 rg 一致的匹配区间（Qt UTF-16 坐标）；无 rg 或 rg 失败时回退 Python re。"""
        q = self._search_needle().strip()
        if not q:
            return []
        key = (
            plain,
            q,
            self.case_cb.isChecked(),
            self.regex_cb.isChecked(),
            self.whole_word_cb.isChecked(),
        )
        if self._spans_cache_key == key:
            return self._spans_cache_spans
        spans: List[Tuple[int, int]] = []
        used_rg = False
        if find_rg_executable():
            sp, err = rg_match_spans_qt(
                plain,
                q,
                self.case_cb.isChecked(),
                self.regex_cb.isChecked(),
                self.whole_word_cb.isChecked(),
            )
            if err is None:
                spans = list(sp or [])
                used_rg = True
        if not used_rg:
            pat = self._compile_pattern()
            if pat is not None:
                max_hits = 3000
                for m in pat.finditer(plain):
                    if len(spans) >= max_hits:
                        break
                    spans.append((m.start(), m.end()))
        self._spans_cache_key = key
        self._spans_cache_spans = spans
        return spans

    def _find_next_pos(self, plain: str, start: int) -> Optional[Tuple[int, int]]:
        spans = self._match_spans_in_plain(plain)
        if not spans:
            return None
        for a, b in spans:
            if a >= start:
                return (a, b)
        return spans[0]

    def _find_prev_pos(self, plain: str, before: int) -> Optional[Tuple[int, int]]:
        spans = self._match_spans_in_plain(plain)
        if not spans:
            return None
        best: Optional[Tuple[int, int]] = None
        for a, b in spans:
            if a < before:
                best = (a, b)
        return best

    def find_next(self):
        plain = self.log_view.toPlainText()
        q = self._search_needle().strip()
        if not q:
            return
        cur = self.log_view.textCursor()
        start = cur.position() + 1 if cur.hasSelection() else cur.position()
        pos = self._find_next_pos(plain, start)
        if pos is None:
            pos = self._find_next_pos(plain, 0)
        if pos is None:
            return
        c = QTextCursor(self.log_view.document())
        c.setPosition(pos[0])
        c.setPosition(pos[1], QTextCursor.MoveMode.KeepAnchor)
        self.log_view.setTextCursor(c)
        self.log_view.centerCursor()
        self._current_match_range = (pos[0], pos[1])
        self._highlight_matches()

    def find_prev(self):
        plain = self.log_view.toPlainText()
        q = self._search_needle().strip()
        if not q:
            return
        cur = self.log_view.textCursor()
        before = cur.selectionStart() if cur.hasSelection() else cur.position()
        pos = self._find_prev_pos(plain, before)
        if pos is None:
            pos = self._find_prev_pos(plain, len(plain))
        if pos is None:
            return
        c = QTextCursor(self.log_view.document())
        c.setPosition(pos[0])
        c.setPosition(pos[1], QTextCursor.MoveMode.KeepAnchor)
        self.log_view.setTextCursor(c)
        self.log_view.centerCursor()
        self._current_match_range = (pos[0], pos[1])
        self._highlight_matches()

    def _collect_match_spans(self, plain: str) -> List[Tuple[int, int]]:
        return self._match_spans_in_plain(plain)

    def _highlight_matches(self):
        q = self._search_needle().strip()
        if not q:
            self.log_view.setExtraSelections([])
            return
        plain = self.log_view.toPlainText()
        spans = self._collect_match_spans(plain)
        fmt_other = QTextCharFormat()
        fmt_other.setBackground(QColor("#5f4b1f"))
        fmt_other.setForeground(QColor("#e7edf6"))

        fmt_curr = QTextCharFormat()
        fmt_curr.setBackground(QColor("#f5b04b"))
        fmt_curr.setForeground(QColor("#242932"))
        fmt_curr.setFontWeight(QFont.Weight.Bold)

        extra: List[QTextEdit.ExtraSelection] = []
        cur_rng = self._current_match_range

        for start, end in spans:
            if cur_rng and start == cur_rng[0] and end == cur_rng[1]:
                continue
            c = QTextCursor(self.log_view.document())
            c.setPosition(start)
            c.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
            sel = QTextEdit.ExtraSelection()
            sel.cursor = c
            sel.format = fmt_other
            extra.append(sel)

        if cur_rng:
            a, b = cur_rng
            if 0 <= a < b <= len(plain):
                c = QTextCursor(self.log_view.document())
                c.setPosition(a)
                c.setPosition(b, QTextCursor.MoveMode.KeepAnchor)
                sel = QTextEdit.ExtraSelection()
                sel.cursor = c
                sel.format = fmt_curr
                extra.append(sel)

        self.log_view.setExtraSelections(extra)


class LogViewerStandaloneDialog(QDialog):
    """仅含检索行 + 正文：加载单文件、rg 目录搜索结果等共用同一布局。"""

    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self.setWindowFlags(
            Qt.WindowType.Window
            | Qt.WindowType.WindowMinimizeButtonHint
            | Qt.WindowType.WindowMaximizeButtonHint
            | Qt.WindowType.WindowCloseButtonHint
        )
        self.setMinimumSize(760, 520)
        self.setStyleSheet("QDialog { background-color: #181b22; }")
        lay = QVBoxLayout(self)
        lay.setContentsMargins(16, 16, 16, 16)
        lay.setSpacing(8)
        self.pane = LogViewerPane(self)
        lay.addWidget(self.pane, 1)

    def set_log(self, title: str, content: str):
        self.setWindowTitle(title)
        self.pane.set_full_text(content, reset_search=True)

