"""
Табличная модель результатов поиска (QAbstractTableModel).

Использование модели вместо QTableWidget обеспечивает высокую
производительность при отображении десятков тысяч найденных совпадений.

Аудит v2:
    * Добавлена роль `MatchRole` для подсветки найденного слова в колонке
      контекста (через `QStyledItemDelegate`).
    * Поддержка пакетного добавления (`add_results`) для снижения числа
      сигналов `beginInsertRows` при большом потоке результатов.
    * Делегат `HighlightDelegate` подсвечивает найденное слово в контексте
      жирным шрифтом и цветом акцента (как в Markdown-экспорте).
"""
from __future__ import annotations

from typing import Any, List

import re

from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
from PySide6.QtGui import QColor, QFont, QFontMetrics
from PySide6.QtWidgets import QStyle, QStyledItemDelegate, QStyleOptionViewItem

from app.config import SearchResult

_HEADERS = ["Файл", "Слово", "Контекст", "Тип", "Изменён", "Путь"]

# Пользовательские роли
MatchRole = Qt.UserRole + 1       # строка с найденным словом для подсветки
PathRole = Qt.UserRole + 2        # полный путь (для tooltip/открытия)


class ResultsTableModel(QAbstractTableModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._rows: List[SearchResult] = []

    # ------------------------------------------------------------------ #
    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:  # noqa: N802
        return 0 if parent.isValid() else len(self._rows)

    def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:  # noqa: N802
        return len(_HEADERS)

    def headerData(self, section, orientation, role=Qt.DisplayRole):  # noqa: N802
        if role != Qt.DisplayRole or orientation != Qt.Horizontal:
            return None
        return _HEADERS[section]

    def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
        if not index.isValid():
            return None
        row = self._rows[index.row()]
        col = index.column()
        if role == Qt.DisplayRole:
            return [row.file_name, row.word, row.context, row.file_type, row.modified, row.full_path][col]
        if role == Qt.ToolTipRole:
            return row.full_path
        if role == MatchRole:
            # Подсвечиваем реально найденную подстроку (для wildcard «охран*»
            # это «охрана»), а не сам шаблон поиска
            return row.matched_text or row.word
        if role == PathRole:
            return row.full_path
        return None

    # ------------------------------------------------------------------ #
    def add_result(self, result: SearchResult) -> None:
        row = len(self._rows)
        self.beginInsertRows(QModelIndex(), row, row)
        self._rows.append(result)
        self.endInsertRows()

    def add_results(self, results: List[SearchResult]) -> None:
        if not results:
            return
        start = len(self._rows)
        self.beginInsertRows(QModelIndex(), start, start + len(results) - 1)
        self._rows.extend(results)
        self.endInsertRows()

    def clear(self) -> None:
        self.beginResetModel()
        self._rows = []
        self.endResetModel()

    def result_at(self, row: int) -> SearchResult:
        return self._rows[row]

    def all_results(self) -> List[SearchResult]:
        return list(self._rows)

    def unique_paths(self) -> List[str]:
        seen: List[str] = []
        seen_set = set()
        for r in self._rows:
            if r.full_path not in seen_set:
                seen_set.add(r.full_path)
                seen.append(r.full_path)
        return seen


class HighlightDelegate(QStyledItemDelegate):
    """Делегат для подсветки найденного слова в колонке контекста.

    Используется только для колонки "Контекст" (index 2) в таблице
    результатов. Слово выделяется жирным шрифтом цвета акцента.
    """

    def __init__(self, accent_color: str = "#6AA1FF", parent=None):
        super().__init__(parent)
        self._accent = QColor(accent_color)

    def paint(self, painter, option: QStyleOptionViewItem, index: QModelIndex):
        word = index.data(MatchRole)
        text = index.data(Qt.DisplayRole)

        # Если нет слова для подсвечивания — обычная отрисовка (сама обрезает
        # текст по ячейке и добавляет многоточие)
        if not word or not text:
            super().paint(painter, option, index)
            return

        # Базовая отрисовка (фон, выделение, фокус), но БЕЗ текста,
        # чтобы избежать наслоения/размытия надписей.
        opt = QStyleOptionViewItem(option)
        self.initStyleOption(opt, index)
        opt.text = ""
        super().paint(painter, opt, index)

        painter.save()
        # КРИТИЧНО: жёстко ограничиваем отрисовку рамкой ячейки, иначе длинный
        # контекст рисуется поверх соседних столбцов (Тип/Изменён/Путь).
        painter.setClipRect(option.rect)

        plain_font = QFont(painter.font())
        bold_font = QFont(plain_font)
        bold_font.setBold(True)
        metrics = QFontMetrics(plain_font)

        rect = option.rect.adjusted(6, 0, -6, 0)

        # Обрезаем текст под ширину ячейки с многоточием (как обычные ячейки).
        # Подсветку слова накладываем уже на укороченный текст.
        display = metrics.elidedText(text, Qt.ElideRight, rect.width())

        # Цвет обычного текста зависит от того, выделена ли строка
        selected = bool(option.state & QStyle.State_Selected)
        normal_color = (
            option.palette.highlightedText().color()
            if selected else option.palette.text().color()
        )
        # На выделении акцентный синий теряет контраст — используем цвет
        # выделенного текста, но оставляем жирное начертание.
        match_color = option.palette.highlightedText().color() if selected else self._accent

        pattern = re.compile(re.escape(word), re.IGNORECASE)
        parts = pattern.split(display)
        matches = pattern.findall(display)

        x = rect.x()
        y = rect.y() + metrics.ascent() + (rect.height() - metrics.height()) // 2
        right = rect.right()

        for i, part in enumerate(parts):
            if part and x <= right:
                painter.setFont(plain_font)
                painter.setPen(normal_color)
                painter.drawText(x, y, part)
                x += metrics.horizontalAdvance(part)

            if i < len(matches) and x <= right:
                painter.setFont(bold_font)
                painter.setPen(match_color)
                painter.drawText(x, y, matches[i])
                x += QFontMetrics(bold_font).horizontalAdvance(matches[i])

        painter.restore()

    def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex):
        size = super().sizeHint(option, index)
        # Минимальная высота строки для читаемости
        size.setHeight(max(24, size.height()))
        return size
