"""
PDF tools page: view, print, sign by selected area, and verify digital signatures.
"""
import gc
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path

from PySide6.QtCore import QPoint, QRect, QSize, QSizeF, Qt, Signal
from PySide6.QtGui import QColor, QCursor, QFont, QIcon, QImage, QPainter, QPen, QPixmap
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
from PySide6.QtPrintSupport import QPrintDialog, QPrinter
from PySide6.QtWidgets import (
    QApplication,
    QDialog,
    QDialogButtonBox,
    QFileDialog,
    QFrame,
    QHBoxLayout,
    QLabel,
    QMessageBox,
    QScrollArea,
    QSizePolicy,
    QSpinBox,
    QTextEdit,
    QToolButton,
    QVBoxLayout,
    QWidget,
)


ASSETS_DIR = Path(__file__).resolve().parents[1] / "assets"


def _asset_icon(filename: str) -> QIcon:
    path = ASSETS_DIR / filename
    return QIcon(str(path)) if path.exists() else QIcon()


class PdfDocumentCanvas(QWidget):
    """Continuous PDF preview with one selectable signature rectangle."""

    selectionChanged = Signal()
    PAGE_GAP = 22
    OUTER_MARGIN = 24

    def __init__(self, parent=None):
        super().__init__(parent)
        self._pages = []
        self._selection_mode = False
        self._drag_start = QPoint()
        self._selection_rect = QRect()
        self._selection_page = -1
        self._dragging = False
        self.setFixedSize(760, 980)
        self.setMouseTracking(True)
        self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)

    def clear(self):
        self._pages = []
        self._selection_rect = QRect()
        self._selection_page = -1
        self._dragging = False
        self.setFixedSize(760, 980)
        self.update()
        self.selectionChanged.emit()

    def set_pages(self, pages):
        pixmap_pages = []
        max_width = 0
        for page_index, image, page_point_size in pages:
            pixmap = QPixmap.fromImage(image)
            if pixmap.isNull():
                continue
            max_width = max(max_width, pixmap.width())
            pixmap_pages.append({
                "index": page_index,
                "pixmap": pixmap,
                "point_size": page_point_size,
                "rect": QRect(),
            })

        if not pixmap_pages:
            self.clear()
            return

        total_width = max_width + self.OUTER_MARGIN * 2
        y = self.OUTER_MARGIN
        for page in pixmap_pages:
            pixmap = page["pixmap"]
            x = self.OUTER_MARGIN + (max_width - pixmap.width()) // 2
            page["rect"] = QRect(x, y, pixmap.width(), pixmap.height())
            y += pixmap.height() + self.PAGE_GAP

        total_height = max(self.OUTER_MARGIN * 2, y - self.PAGE_GAP + self.OUTER_MARGIN)
        self._pages = pixmap_pages
        self._selection_rect = QRect()
        self._selection_page = -1
        self.setFixedSize(max(1, total_width), max(1, total_height))
        self.update()
        self.selectionChanged.emit()

    def set_selection_mode(self, enabled: bool):
        self._selection_mode = enabled
        self.setCursor(QCursor(Qt.CursorShape.CrossCursor if enabled else Qt.CursorShape.ArrowCursor))
        if enabled:
            self._selection_rect = QRect()
            self._selection_page = -1
            self.update()
            self.selectionChanged.emit()

    def has_selection(self) -> bool:
        return (
            self._selection_page >= 0
            and self._selection_rect.isValid()
            and self._selection_rect.width() >= 12
            and self._selection_rect.height() >= 12
        )

    def signature_box_points(self):
        if not self.has_selection():
            return None

        page = self._page_by_index(self._selection_page)
        if not page:
            return None

        page_rect = page["rect"]
        rect = self._selection_rect.normalized().intersected(page_rect)
        if not rect.isValid() or page_rect.width() <= 0 or page_rect.height() <= 0:
            return None

        point_size = page["point_size"]
        page_w = float(point_size.width())
        page_h = float(point_size.height())

        left = rect.left() - page_rect.left()
        top = rect.top() - page_rect.top()
        right = left + rect.width()
        bottom = top + rect.height()

        x1 = left / page_rect.width() * page_w
        x2 = right / page_rect.width() * page_w
        y_top = top / page_rect.height() * page_h
        y_bottom = bottom / page_rect.height() * page_h
        return self._selection_page, (x1, page_h - y_bottom, x2, page_h - y_top)

    def page_top(self, page_index: int) -> int:
        page = self._page_by_index(page_index)
        return page["rect"].top() if page else 0

    def page_at_y(self, y: int) -> int:
        if not self._pages:
            return 0
        for page in self._pages:
            rect = page["rect"]
            if rect.top() <= y <= rect.bottom():
                return page["index"]
        return min(self._pages, key=lambda page: abs(page["rect"].center().y() - y))["index"]

    def _page_by_index(self, page_index: int):
        for page in self._pages:
            if page["index"] == page_index:
                return page
        return None

    def _page_at_pos(self, pos: QPoint):
        for page in self._pages:
            if page["rect"].contains(pos):
                return page
        return None

    def _clamp_to_page(self, pos: QPoint, page_rect: QRect) -> QPoint:
        return QPoint(
            max(page_rect.left(), min(page_rect.right(), pos.x())),
            max(page_rect.top(), min(page_rect.bottom(), pos.y())),
        )

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), QColor("#f8fafc"))

        if not self._pages:
            painter.setPen(QColor("#64748b"))
            painter.setFont(QFont("Segoe UI", 11))
            painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "Chọn file PDF để xem")
            return

        for page in self._pages:
            rect = page["rect"]
            painter.fillRect(rect.adjusted(-1, -1, 1, 1), QColor("#cbd5e1"))
            painter.fillRect(rect, QColor("white"))
            painter.drawPixmap(rect.topLeft(), page["pixmap"])

        if self.has_selection() or (self._dragging and self._selection_rect.isValid()):
            page = self._page_by_index(self._selection_page)
            if page:
                rect = self._selection_rect.normalized().intersected(page["rect"])
                painter.fillRect(rect, QColor(20, 184, 166, 42))
                pen = QPen(QColor("#0f766e"), 2, Qt.PenStyle.DashLine)
                painter.setPen(pen)
                painter.drawRect(rect.adjusted(0, 0, -1, -1))

    def mousePressEvent(self, event):
        if self._selection_mode and event.button() == Qt.MouseButton.LeftButton and self._pages:
            page = self._page_at_pos(event.position().toPoint())
            if page:
                self._dragging = True
                self._selection_page = page["index"]
                self._drag_start = event.position().toPoint()
                self._selection_rect = QRect(self._drag_start, QSize())
                self.update()
                self.selectionChanged.emit()
                return
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self._dragging:
            page = self._page_by_index(self._selection_page)
            if not page:
                return
            end_pos = self._clamp_to_page(event.position().toPoint(), page["rect"])
            self._selection_rect = QRect(self._drag_start, end_pos).normalized().intersected(page["rect"])
            self.update()
            self.selectionChanged.emit()
            return
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self._dragging and event.button() == Qt.MouseButton.LeftButton:
            self._dragging = False
            if not self.has_selection():
                self._selection_rect = QRect()
                self._selection_page = -1
            self.update()
            self.selectionChanged.emit()
            return
        super().mouseReleaseEvent(event)


class PDFToolsPage(QWidget):
    """Tab chuc nang PDF trong ung dung."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.document = QPdfDocument(self)
        self.fitz_document = None
        self.current_pdf_path = ""
        self.current_page = 0
        self.zoom_factor = 1.0
        self._syncing_page = False
        self.signature_overlays = []

        self._build_ui()
        self._update_actions()

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(18, 14, 18, 18)
        root.setSpacing(10)

        toolbar = QFrame()
        toolbar.setObjectName("pdf_toolbar")
        toolbar.setStyleSheet(
            "#pdf_toolbar { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; }"
            "QToolButton, QPushButton { padding: 7px 10px; border: 1px solid #cbd5e1; border-radius: 6px; background: #ffffff; }"
            "QToolButton:hover, QPushButton:hover { background: #f8fafc; }"
            "QToolButton:checked, QPushButton:checked { background: #ccfbf1; border-color: #14b8a6; color: #0f766e; }"
        )
        bar = QHBoxLayout(toolbar)
        bar.setContentsMargins(10, 8, 10, 8)
        bar.setSpacing(8)

        self.btn_open = self._make_button("Mở thư mục", "action_folder_128.png", "Chọn file PDF")
        self.btn_open.clicked.connect(self.open_pdf)
        bar.addWidget(self.btn_open)

        self.btn_prev = self._make_tool_button("‹ Trước", "", "Trang trước")
        self.btn_prev.clicked.connect(self.prev_page)
        bar.addWidget(self.btn_prev)

        self.page_spin = QSpinBox()
        self.page_spin.setMinimum(1)
        self.page_spin.setMaximum(1)
        self.page_spin.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
        self.page_spin.setFixedWidth(58)
        self.page_spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.page_spin.valueChanged.connect(self._on_page_spin_changed)
        bar.addWidget(self.page_spin)

        self.page_total_lbl = QLabel("/ 0")
        self.page_total_lbl.setMinimumWidth(42)
        self.page_total_lbl.setStyleSheet("color: #475569;")
        bar.addWidget(self.page_total_lbl)

        self.btn_next = self._make_tool_button("Sau ›", "", "Trang sau")
        self.btn_next.clicked.connect(self.next_page)
        bar.addWidget(self.btn_next)

        bar.addSpacing(8)
        self.btn_zoom_out = self._make_tool_button("− Thu nhỏ", "", "Thu nhỏ")
        self.btn_zoom_out.clicked.connect(lambda: self.adjust_zoom(-0.15))
        bar.addWidget(self.btn_zoom_out)

        self.zoom_lbl = QLabel("100%")
        self.zoom_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.zoom_lbl.setMinimumWidth(52)
        self.zoom_lbl.setStyleSheet("font-weight: 600; color: #0f172a;")
        bar.addWidget(self.zoom_lbl)

        self.btn_zoom_in = self._make_tool_button("+ Phóng to", "", "Phóng to")
        self.btn_zoom_in.clicked.connect(lambda: self.adjust_zoom(0.15))
        bar.addWidget(self.btn_zoom_in)

        bar.addSpacing(8)
        self.btn_print = self._make_tool_button("In", "action_print_128.png", "In file PDF")
        self.btn_print.clicked.connect(self.print_pdf)
        bar.addWidget(self.btn_print)

        self.btn_select_signature = self._make_button("Vẽ vùng ký", "action_sign_128.png", "Kéo chuột trên trang PDF để chọn vùng ký")
        self.btn_select_signature.setCheckable(True)
        self.btn_select_signature.toggled.connect(self.toggle_signature_selection)
        bar.addWidget(self.btn_select_signature)

        self.btn_sign = self._make_button("Ký file", "action_check_128.png", "Ký số vào vùng đã chọn")
        self.btn_sign.clicked.connect(self.sign_selected_area)
        bar.addWidget(self.btn_sign)

        self.btn_verify = self._make_button("Kiểm tra chữ ký số", "baomat_128.png", "Kiểm tra chữ ký số trong file đang mở")
        self.btn_verify.clicked.connect(self.verify_signature)
        bar.addWidget(self.btn_verify)

        bar.addStretch()
        root.addWidget(toolbar)

        info_row = QHBoxLayout()
        info_row.setContentsMargins(2, 0, 2, 0)
        self.file_lbl = QLabel("Chưa mở file PDF")
        self.file_lbl.setStyleSheet("color: #334155;")
        self.file_lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        info_row.addWidget(self.file_lbl, 1)

        self.status_lbl = QLabel("Sẵn sàng")
        self.status_lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.status_lbl.setStyleSheet("color: #64748b;")
        info_row.addWidget(self.status_lbl)
        root.addLayout(info_row)

        self.scroll = QScrollArea()
        self.scroll.setWidgetResizable(False)
        self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.scroll.setFrameShape(QFrame.Shape.NoFrame)
        self.scroll.setStyleSheet("QScrollArea { background: #e2e8f0; border: 1px solid #cbd5e1; border-radius: 8px; }")
        self.canvas = PdfDocumentCanvas()
        self.canvas.selectionChanged.connect(self._on_selection_changed)
        self.scroll.setWidget(self.canvas)
        self.scroll.verticalScrollBar().valueChanged.connect(self._on_scroll_changed)
        root.addWidget(self.scroll, 1)

    def _make_tool_button(self, text: str, icon_file: str, tooltip: str) -> QToolButton:
        btn = QToolButton()
        btn.setText(text)
        icon = _asset_icon(icon_file) if icon_file else QIcon()
        if not icon.isNull():
            btn.setIcon(icon)
            btn.setIconSize(QSize(20, 20))
            btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        btn.setToolTip(tooltip)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        btn.setFixedHeight(36)
        btn.setMinimumWidth(58)
        return btn

    def _make_button(self, text: str, icon_file: str, tooltip: str) -> QToolButton:
        btn = QToolButton()
        btn.setText(text)
        icon = _asset_icon(icon_file) if icon_file else QIcon()
        if not icon.isNull():
            btn.setIcon(icon)
            btn.setIconSize(QSize(19, 19))
        btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        btn.setToolTip(tooltip)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        btn.setFixedHeight(36)
        btn.setMinimumWidth(max(92, len(text) * 8 + 44))
        return btn

    def open_pdf(self):
        start_dir = os.path.dirname(self.current_pdf_path) if self.current_pdf_path else os.getcwd()
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Chọn file PDF",
            start_dir,
            "PDF files (*.pdf);;All files (*.*)",
        )
        if file_path:
            self.load_pdf(file_path)

    def load_pdf(self, file_path: str):
        if not os.path.exists(file_path):
            QMessageBox.warning(self, "Không tìm thấy file", "File PDF không tồn tại.")
            return

        old_doc = self.document
        try:
            if hasattr(old_doc, "close"):
                old_doc.close()
        except Exception:
            pass

        self.document = QPdfDocument(self)
        error = self.document.load(file_path)
        fitz_ok = self._open_fitz_document(file_path)
        if error != QPdfDocument.Error.None_ and not fitz_ok:
            self.canvas.clear()
            QMessageBox.critical(self, "Không mở được PDF", self._pdf_error_text(error))
            self._update_actions()
            return

        page_count = self._page_count()
        if page_count <= 0:
            self.canvas.clear()
            QMessageBox.critical(self, "KhÃ´ng má»Ÿ Ä‘Æ°á»£c PDF", "File PDF khÃ´ng cÃ³ trang há»£p lá»‡.")
            self._update_actions()
            return

        self.current_pdf_path = file_path
        self.current_page = 0
        self.zoom_factor = 1.0
        self.signature_overlays = self._load_signature_overlays(file_path)
        self.file_lbl.setText(file_path)
        self.page_spin.setMaximum(max(1, page_count))
        self.page_total_lbl.setText(f"/ {page_count}")
        self.btn_select_signature.setChecked(False)
        self.render_document()
        sig_count = len(self.signature_overlays)
        self.status_lbl.setText(f"Đã mở PDF - có {sig_count} chữ ký số" if sig_count else "Đã mở PDF")
        self._update_actions()

    def _close_fitz_document(self):
        doc = getattr(self, "fitz_document", None)
        if doc is not None:
            try:
                doc.close()
            except Exception:
                pass
        self.fitz_document = None

    def _open_fitz_document(self, file_path: str) -> bool:
        self._close_fitz_document()
        try:
            import fitz

            self.fitz_document = fitz.open(file_path)
            return True
        except Exception as exc:
            self.fitz_document = None
            print(f"[PDFTools] PyMuPDF renderer unavailable: {exc}")
            return False

    def _page_count(self) -> int:
        if self.fitz_document is not None:
            try:
                return int(self.fitz_document.page_count)
            except Exception:
                pass
        try:
            return int(self.document.pageCount())
        except Exception:
            return 0

    def _page_point_size(self, page_index: int):
        if self.fitz_document is not None:
            try:
                rect = self.fitz_document.load_page(page_index).rect
                return QSizeF(float(rect.width), float(rect.height))
            except Exception:
                pass
        return self.document.pagePointSize(page_index)

    def _render_page_image(self, page_index: int, target_size: QSize) -> QImage:
        if self.fitz_document is not None:
            try:
                import fitz

                page = self.fitz_document.load_page(page_index)
                rect = page.rect
                zoom_x = target_size.width() / max(1.0, float(rect.width))
                zoom_y = target_size.height() / max(1.0, float(rect.height))
                pixmap = page.get_pixmap(
                    matrix=fitz.Matrix(max(0.01, zoom_x), max(0.01, zoom_y)),
                    alpha=False,
                    annots=True,
                )
                image_format = QImage.Format.Format_RGB888
                if pixmap.n == 4:
                    image_format = QImage.Format.Format_RGBA8888
                elif pixmap.n == 1:
                    image_format = QImage.Format.Format_Grayscale8
                return QImage(pixmap.samples, pixmap.width, pixmap.height, pixmap.stride, image_format).copy()
            except Exception as exc:
                print(f"[PDFTools] PyMuPDF render fallback page {page_index + 1}: {exc}")

        return self.document.render(page_index, target_size, self._render_options())

    def _pdf_error_text(self, error) -> str:
        mapping = {
            QPdfDocument.Error.FileNotFound: "Không tìm thấy file PDF.",
            QPdfDocument.Error.InvalidFileFormat: "File PDF không đúng định dạng hoặc đã bị lỗi.",
            QPdfDocument.Error.IncorrectPassword: "File PDF đang được bảo vệ bằng mật khẩu.",
            QPdfDocument.Error.UnsupportedSecurityScheme: "File PDF dùng cơ chế bảo mật chưa được hỗ trợ.",
            QPdfDocument.Error.DataNotYetAvailable: "Dữ liệu PDF chưa sẵn sàng.",
        }
        return mapping.get(error, "Không thể đọc file PDF này.")

    def render_document(self):
        page_count = self._page_count()
        if not self.current_pdf_path or page_count <= 0:
            self.canvas.clear()
            return

        self.current_page = max(0, min(self.current_page, page_count - 1))
        screen_scale = 96.0 / 72.0
        pages = []
        for page_index in range(page_count):
            page_size = self._page_point_size(page_index)
            width = max(120, int(page_size.width() * screen_scale * self.zoom_factor))
            height = max(120, int(page_size.height() * screen_scale * self.zoom_factor))
            image = self._render_page_image(page_index, QSize(width, height))
            if not image.isNull():
                pages.append((page_index, image, page_size))
            if page_index % 3 == 0:
                QApplication.processEvents()

        if not pages:
            QMessageBox.warning(self, "Lỗi hiển thị", "Không render được file PDF hiện tại.")
            return

        self.canvas.set_pages(pages)
        self._syncing_page = True
        self.page_spin.setValue(self.current_page + 1)
        self._syncing_page = False
        self.zoom_lbl.setText(f"{int(self.zoom_factor * 100)}%")
        self.scroll_to_page(self.current_page)
        self.status_lbl.setText(f"Trang {self.current_page + 1}/{page_count}")
        self._update_actions()

    def _on_page_spin_changed(self, value: int):
        if self._syncing_page:
            return
        self.current_page = value - 1
        self.scroll_to_page(self.current_page)

    def prev_page(self):
        if self.current_page > 0:
            self.current_page -= 1
            self.scroll_to_page(self.current_page)

    def next_page(self):
        if self.current_pdf_path and self.current_page < self._page_count() - 1:
            self.current_page += 1
            self.scroll_to_page(self.current_page)

    def adjust_zoom(self, delta: float):
        if not self.current_pdf_path:
            return
        self.zoom_factor = max(0.35, min(3.0, self.zoom_factor + delta))
        self.render_document()

    def scroll_to_page(self, page_index: int):
        if not self.current_pdf_path:
            return
        page_count = self._page_count()
        self.current_page = max(0, min(page_index, page_count - 1))
        self._syncing_page = True
        self.page_spin.setValue(self.current_page + 1)
        top = max(0, self.canvas.page_top(self.current_page) - 12)
        self.scroll.verticalScrollBar().setValue(top)
        self._syncing_page = False
        self.status_lbl.setText(f"Trang {self.current_page + 1}/{page_count}")
        self._update_actions()

    def _on_scroll_changed(self, value: int):
        if self._syncing_page or not self.current_pdf_path:
            return
        center_y = value + self.scroll.viewport().height() // 2
        page_index = self.canvas.page_at_y(center_y)
        if page_index == self.current_page:
            return
        self.current_page = page_index
        self._syncing_page = True
        self.page_spin.setValue(self.current_page + 1)
        self._syncing_page = False
        self.status_lbl.setText(f"Trang {self.current_page + 1}/{self._page_count()}")
        self._update_actions()

    def toggle_signature_selection(self, checked: bool):
        if checked and not self.current_pdf_path:
            self.btn_select_signature.setChecked(False)
            QMessageBox.information(self, "Chưa có PDF", "Vui lòng mở file PDF trước khi chọn vùng ký.")
            return
        self.canvas.set_selection_mode(checked)
        self.status_lbl.setText("Kéo chuột trên trang PDF để chọn vùng ký" if checked else "Sẵn sàng")
        self._update_actions()

    def _on_selection_changed(self):
        if self.canvas.has_selection():
            self.status_lbl.setText("Đã chọn vùng ký. Bấm Ký file để ký số.")
        self._update_actions()

    def print_pdf(self):
        if not self.current_pdf_path:
            QMessageBox.information(self, "Chưa có PDF", "Vui lòng mở file PDF trước khi in.")
            return

        printer = QPrinter(QPrinter.PrinterMode.HighResolution)
        printer.setDocName(os.path.basename(self.current_pdf_path))
        try:
            printer.setFromTo(1, self._page_count())
        except Exception:
            pass

        dialog = QPrintDialog(printer, self)
        dialog.setWindowTitle("In file PDF")
        if dialog.exec() != QDialog.DialogCode.Accepted:
            return

        from_page, to_page = self._printer_page_range(printer)
        painter = QPainter()
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            if not painter.begin(printer):
                QMessageBox.critical(self, "Lỗi in", "Không thể kết nối tới máy in đã chọn.")
                return

            for index, page_index in enumerate(range(from_page, to_page + 1)):
                if index:
                    printer.newPage()
                target_rect = self._printer_target_rect(printer, painter, page_index)
                image = self._render_page_image(page_index, target_rect.size())
                if image.isNull():
                    continue
                painter.drawImage(target_rect, image)
            painter.end()
            self.status_lbl.setText("Đã gửi lệnh in")
        except Exception as exc:
            if painter.isActive():
                painter.end()
            QMessageBox.critical(self, "Lỗi in PDF", str(exc))
        finally:
            QApplication.restoreOverrideCursor()

    def _printer_page_range(self, printer: QPrinter):
        page_count = self._page_count()
        from_page = printer.fromPage()
        to_page = printer.toPage()
        if from_page <= 0 or to_page <= 0:
            return 0, max(0, page_count - 1)
        return max(0, from_page - 1), min(page_count - 1, to_page - 1)

    def _printer_target_rect(self, printer: QPrinter, painter: QPainter, page_index: int) -> QRect:
        try:
            page_rect = printer.pageRect(QPrinter.Unit.DevicePixel).toRect()
        except Exception:
            page_rect = painter.viewport()

        page_size = self._page_point_size(page_index)
        page_ratio = float(page_size.width()) / max(1.0, float(page_size.height()))
        rect_ratio = page_rect.width() / max(1, page_rect.height())

        if rect_ratio > page_ratio:
            height = page_rect.height()
            width = int(height * page_ratio)
        else:
            width = page_rect.width()
            height = int(width / page_ratio)

        left = page_rect.left() + (page_rect.width() - width) // 2
        top = page_rect.top() + (page_rect.height() - height) // 2
        return QRect(left, top, max(1, width), max(1, height))

    def _render_options(self):
        options = QPdfDocumentRenderOptions()
        options.setRenderFlags(QPdfDocumentRenderOptions.RenderFlag.Annotations)
        return options

    def _load_signature_overlays(self, pdf_path: str):
        overlays = []
        try:
            from pyhanko.pdf_utils.reader import PdfFileReader

            with open(pdf_path, "rb") as file_obj:
                reader = PdfFileReader(file_obj, strict=False)
                page_index_by_ref = {}

                def _walk_pages(node_ref):
                    node = node_ref.get_object() if hasattr(node_ref, "get_object") else node_ref
                    node_type = str(node.get("/Type", ""))
                    if node_type == "/Page":
                        ref_id = getattr(node_ref, "idnum", None)
                        ref_gen = getattr(node_ref, "generation", None)
                        if ref_id is not None:
                            page_index_by_ref[(ref_id, ref_gen)] = len(page_index_by_ref)
                        return
                    for child_ref in node.get("/Kids", []):
                        _walk_pages(child_ref)

                _walk_pages(reader.root["/Pages"])

                for sig in reader.embedded_regular_signatures or []:
                    sig_field = getattr(sig, "sig_field", None)
                    if not sig_field:
                        continue
                    rect = sig_field.get("/Rect")
                    if not rect or len(rect) != 4:
                        continue

                    page_ref = sig_field.get("/P")
                    page_key = (getattr(page_ref, "idnum", None), getattr(page_ref, "generation", None))
                    page_index = page_index_by_ref.get(page_key)
                    if page_index is None:
                        page_index = self._find_signature_page_by_annotation(reader, sig_field)
                    if page_index is None:
                        continue

                    subject = ""
                    mst = ""
                    try:
                        cert = getattr(sig, "signer_cert", None)
                        if cert is not None:
                            subject = cert.subject.human_friendly
                            mst = self._extract_mst_from_subject(subject)
                    except Exception:
                        pass

                    signed_time = ""
                    try:
                        ts = getattr(sig, "self_reported_timestamp", None)
                        signed_time = str(ts) if ts else ""
                    except Exception:
                        pass

                    overlays.append({
                        "page": int(page_index),
                        "rect": tuple(float(value) for value in rect),
                        "field": str(getattr(sig, "field_name", "") or sig_field.get("/T", "")),
                        "subject": subject,
                        "mst": mst,
                        "time": signed_time,
                    })
        except Exception as exc:
            print(f"[PDFTools] Không đọc được lớp chữ ký số: {exc}")
        return overlays

    def _extract_mst_from_subject(self, subject: str) -> str:
        subject = str(subject or "")
        match = re.search(r"MST[\:\s]*([0-9\-]{10,14})", subject, re.IGNORECASE)
        if match:
            return match.group(1).replace("-", "")
        match = re.search(
            r"(?:OID\.0\.9\.2342\.19200300\.100\.1\.1|UID|SERIALNUMBER)\s*=\s*(?:MST:)?([0-9\-]{10,14})",
            subject,
            re.IGNORECASE,
        )
        if match:
            return match.group(1).replace("-", "")
        match = re.search(r"\b([0-9]{10}(?:[0-9]{3})?)\b", subject)
        return match.group(1) if match else ""

    def _find_signature_page_by_annotation(self, reader, sig_field) -> int | None:
        try:
            pages = []

            def _walk_pages(node_ref):
                node = node_ref.get_object() if hasattr(node_ref, "get_object") else node_ref
                if str(node.get("/Type", "")) == "/Page":
                    pages.append(node)
                    return
                for child_ref in node.get("/Kids", []):
                    _walk_pages(child_ref)

            _walk_pages(reader.root["/Pages"])
            target_name = str(sig_field.get("/T", ""))
            target_rect = [float(value) for value in sig_field.get("/Rect", [])]
            for page_index, page in enumerate(pages):
                for annot_ref in page.get("/Annots", []) or []:
                    annot = annot_ref.get_object() if hasattr(annot_ref, "get_object") else annot_ref
                    if str(annot.get("/T", "")) == target_name:
                        return page_index
                    annot_rect = [float(value) for value in annot.get("/Rect", []) or []]
                    if target_rect and annot_rect == target_rect:
                        return page_index
        except Exception:
            pass
        return None

    def _paint_signature_overlays(self, image: QImage, page_index: int, page_size):
        overlays = [item for item in self.signature_overlays if item.get("page") == page_index]
        if not overlays:
            return

        page_w = max(1.0, float(page_size.width()))
        page_h = max(1.0, float(page_size.height()))
        img_w = max(1, image.width())
        img_h = max(1, image.height())

        painter = QPainter(image)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        try:
            for item in overlays:
                x1, y1, x2, y2 = item["rect"]
                left = int(x1 / page_w * img_w)
                top = int((page_h - y2) / page_h * img_h)
                right = int(x2 / page_w * img_w)
                bottom = int((page_h - y1) / page_h * img_h)
                rect = QRect(left, top, max(90, right - left), max(42, bottom - top)).intersected(
                    QRect(0, 0, img_w, img_h)
                )
                if not rect.isValid():
                    continue

                painter.fillRect(rect, QColor(236, 253, 245, 232))
                painter.setPen(QPen(QColor("#047857"), max(1, int(img_w / max(page_w, 1) * 0.9))))
                painter.drawRect(rect.adjusted(1, 1, -2, -2))

                font_size = max(7, min(13, int(rect.height() / 7)))
                title_font = QFont("Segoe UI", font_size + 1, QFont.Weight.Bold)
                body_font = QFont("Segoe UI", font_size)
                text_rect = rect.adjusted(6, 4, -6, -4)

                painter.setPen(QColor("#065f46"))
                painter.setFont(title_font)
                painter.drawText(text_rect, Qt.AlignmentFlag.AlignTop | Qt.TextFlag.TextWordWrap, "ĐÃ KÝ SỐ")

                subject = self._compact_subject(item.get("subject", ""))
                mst = item.get("mst") or ""
                lines = []
                if subject:
                    lines.append(subject)
                if mst:
                    lines.append(f"MST: {mst}")
                if item.get("time"):
                    lines.append(item["time"][:19])
                if not lines:
                    lines.append(item.get("field") or "Chữ ký số")

                painter.setFont(body_font)
                body_top = text_rect.top() + int(font_size * 2.0)
                painter.drawText(
                    QRect(text_rect.left(), body_top, text_rect.width(), max(10, text_rect.bottom() - body_top)),
                    Qt.AlignmentFlag.AlignTop | Qt.TextFlag.TextWordWrap,
                    "\n".join(lines[:3]),
                )
        finally:
            painter.end()

    def _compact_subject(self, subject: str) -> str:
        subject = str(subject or "").strip()
        if not subject:
            return ""
        for token in ("Common Name:", "Organization:"):
            marker = token.lower()
            lower = subject.lower()
            if marker in lower:
                start = lower.find(marker) + len(marker)
                tail = subject[start:].strip()
                return tail.split(",")[0].strip()[:90]
        return subject.split(",")[0].strip()[:90]

    def sign_selected_area(self):
        if not self.current_pdf_path:
            QMessageBox.information(self, "Chưa có PDF", "Vui lòng mở file PDF trước khi ký.")
            return

        selection = self.canvas.signature_box_points()
        if not selection:
            QMessageBox.information(self, "Chưa chọn vùng ký", "Bấm Chọn vùng ký rồi kéo chuột trên trang PDF để khoanh vùng ký.")
            return
        selected_page, box = selection

        width_pt = box[2] - box[0]
        height_pt = box[3] - box[1]
        if width_pt < 90 or height_pt < 48:
            QMessageBox.warning(self, "Vùng ký quá nhỏ", "Vùng ký đang quá nhỏ. Vui lòng chọn vùng lớn hơn để thông tin chữ ký hiển thị rõ.")
            return

        default_path = self._default_signed_path(self.current_pdf_path)
        output_path, _ = QFileDialog.getSaveFileName(
            self,
            "Lưu file PDF đã ký",
            default_path,
            "PDF files (*.pdf)",
        )
        if not output_path:
            return
        if not output_path.lower().endswith(".pdf"):
            output_path += ".pdf"

        cursor_set = False
        try:
            sign_dir = self._resolve_sign_dir()
            if not sign_dir:
                QMessageBox.critical(self, "Lỗi ký số", "Không tìm thấy thư mục ký số sign/.")
                return
            if sign_dir not in sys.path:
                sys.path.insert(0, sign_dir)

            from win_signer import select_signer
            from sign_pdf import PDFSigner

            signer_obj = select_signer()
            if not signer_obj:
                self.status_lbl.setText("Đã hủy chọn chứng chỉ")
                return

            # Ký thủ công trên tab PDF ưu tiên ổn định: không gọi TSA để tránh lỗi
            # "Error in communication with timestamp server" làm hỏng thao tác ký.
            effective_ts_url = None
            self.status_lbl.setText("Đang ký file PDF...")
            QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
            cursor_set = True
            QApplication.processEvents()

            pdf_engine = PDFSigner(None, None)
            pdf_engine.sign_pdf_native_box(
                in_file=self.current_pdf_path,
                out_file=output_path,
                page_num=selected_page,
                box=box,
                signer=signer_obj,
                ts_url=effective_ts_url,
                use_logo=True,
                archive_ts=False,
            )

            del signer_obj
            gc.collect()
            time.sleep(0.2)
        except ImportError as exc:
            QMessageBox.critical(self, "Lỗi import module ký", f"{exc}\nVui lòng kiểm tra thư mục sign và dependencies.")
            return
        except Exception as exc:
            QMessageBox.critical(self, "Lỗi ký PDF", str(exc))
            return
        finally:
            if cursor_set:
                QApplication.restoreOverrideCursor()

        if os.path.exists(output_path):
            QMessageBox.information(self, "Ký thành công", f"Đã ký và lưu file:\n{output_path}")
            self.load_pdf(output_path)
        else:
            QMessageBox.warning(self, "Cảnh báo", "Quy trình ký đã chạy nhưng chưa thấy file đầu ra.")

    def verify_signature(self):
        if not self.current_pdf_path:
            QMessageBox.information(self, "Chưa có PDF", "Vui lòng mở file PDF trước khi kiểm tra chữ ký.")
            return

        try:
            from core.pdf_signature_verifier import verify_pdf_signature

            result = verify_pdf_signature(self.current_pdf_path)
        except Exception as exc:
            QMessageBox.critical(self, "Lỗi kiểm tra chữ ký", str(exc))
            return

        if not result.get("is_signed"):
            QMessageBox.information(self, "Kết quả kiểm tra", "File PDF hiện tại chưa có chữ ký số.")
            return

        signatures = result.get("signatures", [])
        valid = bool(result.get("is_valid"))
        title = "Chữ ký hợp lệ" if valid else "Cần kiểm tra lại chữ ký"
        text = (
            f"File có {len(signatures)} chữ ký số.\n"
            f"Kết quả: {'Hợp lệ, nội dung chưa bị thay đổi.' if valid else 'Có chữ ký chưa xác minh được toàn vẹn.'}"
        )
        details = []
        for idx, sig in enumerate(signatures, 1):
            details.append(f"Chữ ký {idx}")
            details.append(f"Người ký: {sig.get('subject') or 'Không rõ'}")
            details.append(f"MST: {sig.get('mst') or 'Không có'}")
            details.append(f"Toàn vẹn: {'Có' if sig.get('intact') else 'Không'}")
            if sig.get("signing_time"):
                details.append(f"Thời gian ký: {sig.get('signing_time')}")
            details.append("")

        self._show_signature_result_dialog(title, text, "\n".join(details).strip(), valid)

    def _show_signature_result_dialog(self, title: str, summary: str, details: str, valid: bool):
        dialog = QDialog(self)
        dialog.setWindowTitle(title)
        dialog.setMinimumSize(760, 520)

        layout = QVBoxLayout(dialog)
        layout.setContentsMargins(18, 16, 18, 14)
        layout.setSpacing(12)

        icon_text = "✓" if valid else "!"
        icon_color = "#047857" if valid else "#b45309"
        header = QLabel(f"{icon_text}  {summary}")
        header.setWordWrap(True)
        header.setFont(QFont("Segoe UI", 12, QFont.Weight.DemiBold))
        header.setStyleSheet(f"color: {icon_color};")
        layout.addWidget(header)

        details_box = QTextEdit()
        details_box.setReadOnly(True)
        details_box.setPlainText(details or "Không có chi tiết chữ ký.")
        details_box.setFont(QFont("Consolas", 10))
        details_box.setStyleSheet(
            "QTextEdit { background: #ffffff; border: 1px solid #cbd5e1; "
            "border-radius: 8px; padding: 10px; color: #0f172a; }"
        )
        layout.addWidget(details_box, 1)

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
        buttons.accepted.connect(dialog.accept)
        layout.addWidget(buttons)
        dialog.exec()

    def _default_signed_path(self, file_path: str) -> str:
        path = Path(file_path)
        candidate = path.with_name(f"{path.stem}_signed{path.suffix}")
        if not candidate.exists():
            return str(candidate)
        suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
        return str(path.with_name(f"{path.stem}_signed_{suffix}{path.suffix}"))

    def _resolve_sign_dir(self):
        if getattr(sys, "frozen", False):
            sign_dir = os.path.join(sys._MEIPASS, "sign")
        else:
            sign_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "sign"))
            if not os.path.isdir(sign_dir):
                sign_dir = os.path.normpath(os.path.join(os.getcwd(), "sign"))
        return sign_dir if os.path.isdir(sign_dir) else ""

    def _load_timestamp_url(self, sign_dir: str):
        ts_url = "http://timestamp.digicert.com"
        yaml_path = os.path.join(sign_dir, "sign.yaml")
        try:
            import yaml

            with open(yaml_path, "r", encoding="utf-8") as yf:
                cfg = yaml.safe_load(yf) or {}
            ts_cfg = cfg.get("signing", {}).get("timestamp", {})
            return ts_cfg.get("url", ts_url) if ts_cfg.get("enabled", True) else None
        except Exception:
            return ts_url

    def _update_actions(self):
        page_count = self._page_count()
        has_pdf = bool(self.current_pdf_path and page_count > 0)
        self.btn_prev.setEnabled(has_pdf and self.current_page > 0)
        self.btn_next.setEnabled(has_pdf and self.current_page < page_count - 1)
        self.page_spin.setEnabled(has_pdf)
        self.btn_zoom_in.setEnabled(has_pdf)
        self.btn_zoom_out.setEnabled(has_pdf)
        self.btn_print.setEnabled(has_pdf)
        self.btn_select_signature.setEnabled(has_pdf)
        self.btn_sign.setEnabled(has_pdf and self.canvas.has_selection())
        self.btn_verify.setEnabled(has_pdf)
