from __future__ import annotations
import logging
from qtpy.QtCore import QPointF, QRectF, Qt, Signal
from qtpy.QtGui import (
    QColor,
    QFont,
    QImage,
    QPainter,
    QPen,
    QPixmap,
)
from qtpy.QtWidgets import (
    QFrame,
    QHBoxLayout,
    QLabel,
    QWidget,
)

from .models import (
    DocumentElement,
    DocumentModel,
    ElementType,
)
from .utils import CoordinateMapper

COLORS = {
    ElementType.TEXT: QColor("#21a366"),
    ElementType.IMAGE: QColor("#e6b800"),
    ElementType.TABLE: QColor("#d32f2f"),
}


class PageEditor(QWidget):
    """PDF page editor with normalized-coordinate selections."""

    selection_created = Signal(object)
    selection_changed = Signal(str, object)
    selection_deleted = Signal(str)
    element_double_clicked = Signal(str)
    page_changed = Signal()

    def __init__(
        self,
        model: DocumentModel,
        parent=None,
    ) -> None:
        super().__init__(parent)

        self.model = model
        self.page_number = 1
        self.tool: ElementType | None = None

        self.page_pixmap = QPixmap()
        self.zoom = 1.0
        self.selected_id = ""

        self.drag_start = QPointF()
        self.original_rect: tuple[float, float, float, float] | None = None
        self.drag_mode = ""
        self.drag_element_id = ""
        self.drawing_rect: QRectF | None = None

        self.setMinimumSize(350, 500)
        self.setMouseTracking(True)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)

    def set_tool(self, tool: ElementType | None) -> None:
        self.tool = tool
        self.setCursor(
            Qt.CursorShape.CrossCursor
            if tool is not None
            else Qt.CursorShape.ArrowCursor
        )

    def set_page(self, page_number: int) -> None:
        self.page_number = page_number
        self.selected_id = ""
        self.drawing_rect = None
        self.drag_mode = ""
        self.update()

    def set_pixmap(self, pixmap: QPixmap) -> None:
        self.page_pixmap = pixmap
        self.update()

    def page_state(self) -> PageState | None:
        if 1 <= self.page_number <= len(self.model.pages):
            return self.model.page(self.page_number)
        return None

    def page_rect(self) -> QRectF:
        page = self.page_state()
        if page is None or page.width_pt <= 0 or page.height_pt <= 0:
            return QRectF()

        available = QRectF(
            10,
            10,
            max(10, self.width() - 20),
            max(10, self.height() - 20),
        )

        base_scale = min(
            available.width() / page.width_pt,
            available.height() / page.height_pt,
        )

        scale = base_scale * self.zoom

        width = page.width_pt * scale
        height = page.height_pt * scale

        left = (self.width() - width) / 2
        top = (self.height() - height) / 2

        return QRectF(left, top, width, height)

    def normalized_rect_to_view(
        self,
        element: DocumentElement,
    ) -> QRectF:
        page_rect = self.page_rect()

        x, y, width, height = (
            CoordinateMapper.normalized_to_view(
                element.x,
                element.y,
                element.width,
                element.height,
                page_rect.left(),
                page_rect.top(),
                page_rect.width(),
                page_rect.height(),
            )
        )

        return QRectF(x, y, width, height)

    def view_to_normalized(
        self,
        point: QPointF,
    ) -> tuple[float, float]:
        rect = self.page_rect()

        return (
            (point.x() - rect.left()) / rect.width(),
            (point.y() - rect.top()) / rect.height(),
        )

    def hit_test(self, point: QPointF) -> DocumentElement | None:
        page = self.page_state()
        if page is None:
            return None

        for element in reversed(page.elements):
            if self.normalized_rect_to_view(element).contains(point):
                return element

        return None

    def paintEvent(self, event) -> None:
        painter = QPainter(self)
        try:
            painter.setRenderHint(QPainter.RenderHint.Antialiasing)
            painter.fillRect(self.rect(), QColor("#202124"))

            page = self.page_state()
            if page is None:
                return

            rect = self.page_rect()

            if not rect.isEmpty():
                painter.setPen(QPen(QColor("#111111"), 1))
                painter.setBrush(QColor("white"))
                painter.drawRect(rect)

                if not self.page_pixmap.isNull():
                    painter.drawPixmap(
                        rect,
                        self.page_pixmap,
                        QRectF(self.page_pixmap.rect()),
                    )

                for element in page.elements:
                    selection = self.normalized_rect_to_view(element)
                    color = COLORS[element.element_type]

                    fill = QColor(color)
                    fill.setAlpha(45)

                    painter.setBrush(fill)

                    pen = QPen(color, 2)
                    if element.id == self.selected_id:
                        pen.setWidth(3)

                    painter.setPen(pen)
                    painter.drawRect(selection)

                    if element.processing:
                        painter.setPen(
                            QPen(
                                QColor("#000000"),
                                1,
                            )
                        )
                        painter.setFont(QFont("Arial", 13))
                        painter.drawText(
                            selection.adjusted(4, 4, -4, -4),
                            Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
                            "Processing…",
                        )

                    if element.error:
                        painter.setPen(QPen(QColor("#b71c1c"), 2))
                        painter.drawText(
                            selection.adjusted(4, 4, -4, -4),
                            Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
                            "Error",
                        )

                    if element.id == self.selected_id:
                        handle = 8.0
                        painter.setBrush(color)
                        painter.drawRect(
                            QRectF(
                                selection.right() - handle,
                                selection.bottom() - handle,
                                handle,
                                handle,
                            )
                        )

                if self.drawing_rect is not None:
                    color = (
                        COLORS[self.tool]
                        if self.tool is not None
                        else QColor("#ffffff")
                    )

                    fill = QColor(color)
                    fill.setAlpha(40)

                    painter.setBrush(fill)
                    painter.setPen(QPen(color, 2))
                    painter.drawRect(self.drawing_rect)
        finally:
            painter.end()

    def _near_resize_handle(
        self,
        rect: QRectF,
        point: QPointF,
    ) -> bool:
        handle = QRectF(
            rect.right() - 14,
            rect.bottom() - 14,
            28,
            28,
        )
        return handle.contains(point)

    def mousePressEvent(self, event) -> None:
        if event.button() != Qt.MouseButton.LeftButton:
            return

        point = event.position()
        self.drag_start = point

        hit = self.hit_test(point)

        if hit:
            self.selected_id = hit.id
            self.drag_element_id = hit.id
            self.original_rect = hit.rect()

            rect = self.normalized_rect_to_view(hit)

            if self._near_resize_handle(rect, point):
                self.drag_mode = "resize"
            else:
                self.drag_mode = "move"

            self.update()
            return

        if self.tool is not None:
            page_rect = self.page_rect()

            if page_rect.contains(point):
                self.drag_mode = "create"
                self.drawing_rect = QRectF(
                    point,
                    point,
                )
                self.update()

    def mouseMoveEvent(self, event) -> None:
        point = event.position()

        if self.drag_mode == "create":
            page_rect = self.page_rect()
            end = QPointF(
                min(max(point.x(), page_rect.left()), page_rect.right()),
                min(max(point.y(), page_rect.top()), page_rect.bottom()),
            )

            self.drawing_rect = QRectF(
                self.drag_start,
                end,
            ).normalized()

            self.update()
            return

        if not self.drag_element_id or self.original_rect is None:
            return

        element = self.model.element(self.drag_element_id)

        if element is None:
            return

        page_rect = self.page_rect()

        if self.drag_mode == "move":
            delta = point - self.drag_start

            dx = delta.x() / page_rect.width()
            dy = delta.y() / page_rect.height()

            element.x = min(
                max(self.original_rect[0] + dx, 0),
                1 - element.width,
            )
            element.y = min(
                max(self.original_rect[1] + dy, 0),
                1 - element.height,
            )

            self.update()
            self.selection_changed.emit(
                element.id,
                element.rect(),
            )

        elif self.drag_mode == "resize":
            start_x = self.original_rect[0]
            start_y = self.original_rect[1]

            nx, ny = self.view_to_normalized(point)

            element.width = max(
                0.01,
                min(1 - start_x, nx - start_x),
            )
            element.height = max(
                0.01,
                min(1 - start_y, ny - start_y),
            )

            self.update()
            self.selection_changed.emit(
                element.id,
                element.rect(),
            )

    def mouseReleaseEvent(self, event) -> None:
        if event.button() != Qt.MouseButton.LeftButton:
            return

        if self.drag_mode == "create":
            if self.drawing_rect is None or self.tool is None:
                return

            page_rect = self.page_rect()

            normalized = CoordinateMapper.view_to_normalized(
                self.drawing_rect.left(),
                self.drawing_rect.top(),
                self.drawing_rect.width(),
                self.drawing_rect.height(),
                page_rect.left(),
                page_rect.top(),
                page_rect.width(),
                page_rect.height(),
            )

            x, y, width, height = normalized

            if width >= 0.01 and height >= 0.01:
                element = DocumentElement(
                    element_type=self.tool,
                    x=x,
                    y=y,
                    width=width,
                    height=height,
                    page_number=self.page_number,
                    processing=True,
                )
                self.selection_created.emit(element)

            self.drawing_rect = None

        elif self.drag_element_id and self.original_rect:
            drag_element = self.model.element(self.drag_element_id)

            if drag_element:
                self.selection_changed.emit(
                    drag_element.id,
                    {
                        "old": self.original_rect,
                        "new": drag_element.rect(),
                    },
                )

        self.drag_mode = ""
        self.drag_element_id = ""
        self.original_rect = None
        self.update()

    def mouseDoubleClickEvent(self, event) -> None:
        element = self.hit_test(event.position())

        if element:
            self.selected_id = element.id
            self.element_double_clicked.emit(element.id)

    def wheelEvent(self, event) -> None:
        delta = event.angleDelta().y()

        if delta > 0:
            self.zoom = min(4.0, self.zoom * 1.1)
        elif delta < 0:
            self.zoom = max(0.4, self.zoom / 1.1)

        self.update()

    def keyPressEvent(self, event) -> None:
        if (
            event.key()
            in (
                Qt.Key.Key_Delete,
                Qt.Key.Key_Backspace,
            )
            and self.selected_id
        ):
            element_id = self.selected_id
            self.selected_id = ""
            self.selection_deleted.emit(element_id)
            self.update()
            return

        if event.key() == Qt.Key.Key_0:
            self.zoom = 1.0
            self.update()
            return

        super().keyPressEvent(event)


class PreviewWidget(QWidget):
    """High-quality approximation of the generated DOCX page."""

    def __init__(
        self,
        model: DocumentModel,
        parent=None,
    ) -> None:
        super().__init__(parent)

        self.model = model
        self.page_number = 1
        self.setMinimumSize(350, 500)

    def set_page(self, page_number: int) -> None:
        self.page_number = page_number
        self.update()

    def page_rect(self) -> QRectF:
        page = self.model.page(self.page_number)

        available = QRectF(
            10,
            10,
            max(10, self.width() - 20),
            max(10, self.height() - 20),
        )

        scale = min(
            available.width() / page.width_pt,
            available.height() / page.height_pt,
        )

        width = page.width_pt * scale
        height = page.height_pt * scale

        return QRectF(
            (self.width() - width) / 2,
            (self.height() - height) / 2,
            width,
            height,
        )

    def paintEvent(self, event) -> None:
        """Paint the generated DOCX page preview safely."""
        painter = QPainter(self)

        if not painter.isActive():
            return

        try:
            painter.setRenderHint(
                QPainter.RenderHint.Antialiasing,
                True,
            )

            painter.fillRect(
                self.rect(),
                QColor("#303134"),
            )

            # Do not attempt to access a page until the model
            # actually contains pages.
            if self.model is None:
                return

            if not self.model.pages:
                return

            # Make sure the requested page exists.
            if (
                self.page_number < 0
                or self.page_number >= len(self.model.pages)
            ):
                return

            page = self.model.pages[self.page_number]

            if page is None:
                return

            rect = self.page_rect()

            if rect.isEmpty():
                return

            painter.setBrush(QColor("white"))
            painter.setPen(
                QPen(QColor("#111111"), 1)
            )
            painter.drawRect(rect)

            for element in page.elements:
                x, y, width, height = (
                    CoordinateMapper.normalized_to_view(
                        element.x,
                        element.y,
                        element.width,
                        element.height,
                        rect.left(),
                        rect.top(),
                        rect.width(),
                        rect.height(),
                    )
                )

                target = QRectF(
                    x,
                    y,
                    width,
                    height,
                )

                if target.isEmpty():
                    continue

                if element.element_type == ElementType.TEXT:
                    self._draw_text(
                        painter,
                        target,
                        element.text or "",
                    )

                elif element.element_type == ElementType.IMAGE:
                    self._draw_image(
                        painter,
                        target,
                        element.image_png_b64 or "",
                    )

                elif element.element_type == ElementType.TABLE:
                    if (
                        element.table_fallback_image
                        and element.image_png_b64
                    ):
                        self._draw_image(
                            painter,
                            target,
                            element.image_png_b64,
                        )
                    elif element.table:
                        self._draw_table(
                            painter,
                            target,
                            element,
                        )

        except Exception:
            # Painting must NEVER propagate exceptions into Qt.
            logging.getLogger(__name__).exception(
                "Error while painting DOCX preview"
            )

        finally:
            if painter.isActive():
                painter.end()

    def _draw_text(
        self,
        painter: QPainter,
        target: QRectF,
        text: str,
    ) -> None:
        font = QFont(
            self.model.font.family,
            max(1, int(self.model.font.size_pt)),
        )
        font.setBold(self.model.font.bold)
        font.setItalic(self.model.font.italic)
        font.setUnderline(self.model.font.underline)

        painter.setFont(font)
        painter.setPen(QColor("#111111"))

        painter.drawText(
            target.adjusted(4, 3, -4, -3),
            Qt.TextFlag.TextWordWrap
            | Qt.AlignmentFlag.AlignLeft
            | Qt.AlignmentFlag.AlignTop,
            text,
        )

    def _draw_image(
        self,
        painter: QPainter,
        target: QRectF,
        image_b64: str,
    ) -> None:
        if not image_b64:
            painter.fillRect(target, QColor("#eeeeee"))
            return

        try:
            import base64

            raw = base64.b64decode(image_b64)
            image = QImage.fromData(raw)
            pixmap = QPixmap.fromImage(image)

            if pixmap.isNull():
                raise ValueError("Invalid image.")

            scaled = pixmap.scaled(
                int(target.width()),
                int(target.height()),
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation,
            )

            x = target.left() + (
                target.width() - scaled.width()
            ) / 2
            y = target.top() + (
                target.height() - scaled.height()
            ) / 2

            painter.drawPixmap(
                int(x),
                int(y),
                scaled,
            )
        except Exception:  # noqa: BLE001
            painter.fillRect(target, QColor("#eeeeee"))

    def _draw_table(
        self,
        painter: QPainter,
        target: QRectF,
        element: DocumentElement,
    ) -> None:
        table = element.table
        if table is None or not table.rows:
            return

        rows = len(table.rows)
        columns = max(
            len(row)
            for row in table.rows
        )

        if columns == 0:
            return

        painter.setPen(QPen(QColor("#444444"), 1))
        painter.setBrush(QColor("#ffffff"))
        painter.drawRect(target)

        row_height = target.height() / rows
        column_width = target.width() / columns

        font = QFont(
            self.model.font.family,
            max(5, int(self.model.font.size_pt * 0.7)),
        )
        font.setBold(self.model.font.bold)
        painter.setFont(font)

        for row_index, row in enumerate(table.rows):
            for column_index in range(columns):
                cell = QRectF(
                    target.left() + column_index * column_width,
                    target.top() + row_index * row_height,
                    column_width,
                    row_height,
                )

                painter.drawRect(cell)

                value = (
                    row[column_index]
                    if column_index < len(row)
                    else ""
                )

                painter.drawText(
                    cell.adjusted(3, 2, -3, -2),
                    Qt.TextFlag.TextWordWrap
                    | Qt.AlignmentFlag.AlignLeft
                    | Qt.AlignmentFlag.AlignVCenter,
                    value,
                )


class PageStatusWidget(QFrame):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)

        self.label = QLabel("Page 1")

        layout = QHBoxLayout(self)
        layout.setContentsMargins(8, 4, 8, 4)
        layout.addWidget(self.label)

    def set_status(
        self,
        page: int,
        total: int,
        processed: bool,
    ) -> None:
        marker = "✓" if processed else "—"
        self.label.setText(
            f"Page {page} / {total}   {marker}"
        )