"""
Read-only QGraphicsView visualization of a WorkflowDefinition.

Sits next to the workflow editor's tree-of-steps and shows the flow
as a top-down node graph. Step nodes are color-coded by step type;
control-flow steps (IF/ELSE, TRY/CATCH, loops) render their children
inside a labelled container. Clicking a node emits a signal carrying
the underlying step so the editor can sync the tree selection.

The widget never edits the workflow — the tree remains the editor of
record. Selection is one-way for editing (tree -> graph or graph ->
tree highlight), so dragging on the graph never reorders steps.
"""

from __future__ import annotations

from typing import Dict, List, Optional, Tuple

from .models import EngineType
from .qt_compat import (
    QBrush,
    QColor,
    QFont,
    QGraphicsItem,
    QGraphicsRectItem,
    QGraphicsScene,
    QGraphicsSimpleTextItem,
    QGraphicsView,
    QPainter,
    QPen,
    Qt,
    pyqtSignal,
)
from .workflow_definition import (
    WorkflowDefinition,
    WorkflowDefinitionStep,
    WorkflowDefinitionStepType,
)


# Layout tuning constants. Sizes are in scene pixels.
_NODE_WIDTH = 220.0
_NODE_HEIGHT = 56.0
_VERTICAL_GAP = 28.0
_BRANCH_HORIZONTAL_GAP = 32.0
_GROUP_PADDING_X = 18.0
_GROUP_PADDING_TOP = 28.0
_GROUP_PADDING_BOTTOM = 14.0
_ARROW_LENGTH = 8.0


_STEP_TYPE_COLORS: Dict[str, str] = {
    WorkflowDefinitionStepType.TASK: "#2563eb",         # blue
    WorkflowDefinitionStepType.SET_VARIABLE: "#0e7490", # teal
    WorkflowDefinitionStepType.FOR_LOOP: "#9333ea",     # purple
    WorkflowDefinitionStepType.IF_ELSE: "#ca8a04",      # amber
    WorkflowDefinitionStepType.LOG: "#475569",          # slate
    WorkflowDefinitionStepType.TRY_CATCH: "#c2410c",    # orange-red
}

_PYTHON_TASK_COLOR = "#16a34a"  # green for Python script tasks


class _StepNodeItem(QGraphicsRectItem):
    """A single rounded rectangle representing one workflow step."""

    def __init__(self, step: WorkflowDefinitionStep, view: "WorkflowGraphView"):
        super().__init__(0.0, 0.0, _NODE_WIDTH, _NODE_HEIGHT)
        self._step = step
        self._view = view
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
        self.setData(0, step.id)

        title_text = step.name or step.step_type
        title = QGraphicsSimpleTextItem(self._truncate(title_text, 32), self)
        title_font = QFont()
        title_font.setBold(True)
        title.setFont(title_font)
        title.setBrush(QBrush(QColor("#ffffff")))
        title.setPos(10.0, 8.0)

        subtitle_text = self._subtitle_text(step)
        subtitle = QGraphicsSimpleTextItem(self._truncate(subtitle_text, 36), self)
        subtitle.setBrush(QBrush(QColor("#e2e8f0")))
        subtitle.setPos(10.0, 28.0)

        if step.description:
            self.setToolTip(step.description)
        else:
            self.setToolTip(f"{step.step_type}: {title_text}")

        self._apply_palette(selected=False)

    @property
    def step(self) -> WorkflowDefinitionStep:
        return self._step

    def mark_selected(self, selected: bool) -> None:
        self._apply_palette(selected=selected)

    def _apply_palette(self, *, selected: bool) -> None:
        base = self._fill_color()
        self.setBrush(QBrush(base))
        if selected:
            pen = QPen(QColor("#0f172a"), 3.0)
        else:
            pen = QPen(QColor("#1e293b"), 1.0)
        self.setPen(pen)

    def _fill_color(self) -> QColor:
        color_hex = _STEP_TYPE_COLORS.get(self._step.step_type, "#334155")
        if self._step.step_type == WorkflowDefinitionStepType.TASK:
            engine = str((self._step.config or {}).get("engine_type", "") or "").strip()
            if engine == EngineType.PYTHON_SCRIPT.value:
                color_hex = _PYTHON_TASK_COLOR
        if not self._step.enabled:
            return QColor("#94a3b8")
        return QColor(color_hex)

    def mousePressEvent(self, event) -> None:  # type: ignore[override]
        if event.button() == Qt.MouseButton.LeftButton:
            self._view.notify_node_clicked(self._step)
        super().mousePressEvent(event)

    @staticmethod
    def _subtitle_text(step: WorkflowDefinitionStep) -> str:
        config = dict(step.config or {})
        if step.step_type == WorkflowDefinitionStepType.TASK:
            engine = str(config.get("engine_type", "") or "").strip() or "(no engine)"
            if engine == EngineType.PYTHON_SCRIPT.value:
                script = str(config.get("script_path", "") or "").strip()
                tail = script.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] if script else "(no script)"
                return f"Python — {tail}"
            model = str(config.get("model_reference", "") or "").strip()
            tail = model.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] if model else "(no model)"
            return f"{engine} — {tail}"
        if step.step_type == WorkflowDefinitionStepType.SET_VARIABLE:
            scope = str(config.get("scope", "local") or "local")
            name = str(config.get("variable_name", "") or "?")
            return f"{scope}: {name}"
        if step.step_type == WorkflowDefinitionStepType.FOR_LOOP:
            loop_type = str(config.get("loop_type", "range") or "range")
            variable = str(config.get("variable_name", "") or "?")
            return f"loop ({loop_type}) {variable}"
        if step.step_type == WorkflowDefinitionStepType.IF_ELSE:
            variable = str(config.get("variable_name", "") or "?")
            operator = str(config.get("operator", "equals") or "equals")
            return f"if {variable} {operator} ..."
        if step.step_type == WorkflowDefinitionStepType.LOG:
            level = str(config.get("level", "info") or "info")
            return f"log [{level}]"
        if step.step_type == WorkflowDefinitionStepType.TRY_CATCH:
            error_var = str(config.get("error_variable", "last_error") or "last_error")
            return f"try / catch -> {error_var}"
        return step.step_type

    @staticmethod
    def _truncate(text: str, limit: int) -> str:
        text = str(text)
        if len(text) <= limit:
            return text
        return text[: max(limit - 1, 1)] + "…"


class _GroupBoxItem(QGraphicsRectItem):
    """A labelled container drawn around a sub-flow (loop body, branch, etc.)."""

    def __init__(self, label: str, fill_hex: str = "#1e293b"):
        super().__init__()
        self.setPen(QPen(QColor(fill_hex), 1.0, Qt.PenStyle.DashLine))
        self.setBrush(QBrush(QColor(0, 0, 0, 0)))
        self._label_text = label
        self._label_item = QGraphicsSimpleTextItem(label, self)
        font = QFont()
        font.setItalic(True)
        font.setBold(True)
        self._label_item.setFont(font)
        self._label_item.setBrush(QBrush(QColor(fill_hex)))

    def set_geometry(self, x: float, y: float, width: float, height: float) -> None:
        self.setRect(x, y, width, height)
        self._label_item.setPos(x + 10.0, y + 6.0)


class WorkflowGraphView(QGraphicsView):
    """Read-only flow visualization for a WorkflowDefinition."""

    nodeClicked = pyqtSignal(object)  # emits the WorkflowDefinitionStep

    def __init__(self, parent=None):
        super().__init__(parent)
        self._scene = QGraphicsScene(self)
        self.setScene(self._scene)
        self.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        self.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
        self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
        self.setBackgroundBrush(QBrush(QColor("#0f172a")))

        self._nodes_by_step_id: Dict[str, _StepNodeItem] = {}
        self._highlighted_step_id: Optional[str] = None

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def set_workflow(self, workflow_definition: Optional[WorkflowDefinition]) -> None:
        """Re-layout the entire graph for *workflow_definition*."""
        self._scene.clear()
        self._nodes_by_step_id.clear()
        self._highlighted_step_id = None

        if workflow_definition is None or not workflow_definition.steps:
            placeholder = QGraphicsSimpleTextItem(
                "No workflow steps yet — add one in the tree on the left."
            )
            placeholder.setBrush(QBrush(QColor("#94a3b8")))
            placeholder.setPos(20.0, 20.0)
            self._scene.addItem(placeholder)
            self._scene.setSceneRect(0.0, 0.0, 480.0, 80.0)
            return

        next_y = 0.0
        max_x = 0.0
        for step in workflow_definition.steps:
            placement = self._place_step_block(step, x=0.0, y=next_y)
            next_y = placement.bottom + _VERTICAL_GAP
            if placement.right > max_x:
                max_x = placement.right

        scene_rect = self._scene.itemsBoundingRect()
        if scene_rect.width() > 0 and scene_rect.height() > 0:
            scene_rect.adjust(-20.0, -20.0, 20.0, 20.0)
            self._scene.setSceneRect(scene_rect)
        else:
            self._scene.setSceneRect(0.0, 0.0, max_x + 40.0, next_y + 40.0)

    def select_step(self, step_id: Optional[str]) -> None:
        """Highlight the node matching *step_id* (called when the tree selection changes)."""
        previous = self._nodes_by_step_id.get(self._highlighted_step_id or "")
        if previous is not None:
            previous.mark_selected(False)
        self._highlighted_step_id = step_id

        if not step_id:
            return
        node = self._nodes_by_step_id.get(step_id)
        if node is None:
            return
        node.mark_selected(True)
        self.ensureVisible(node, 40, 40)

    def notify_node_clicked(self, step: WorkflowDefinitionStep) -> None:
        """Forward a node-click as a Qt signal (called by node items)."""
        self.nodeClicked.emit(step)

    # ------------------------------------------------------------------
    # Layout
    # ------------------------------------------------------------------
    class _Placement:
        __slots__ = ("right", "bottom", "node_center_x", "node_bottom")

        def __init__(self, right: float, bottom: float, node_center_x: float, node_bottom: float):
            self.right = right
            self.bottom = bottom
            self.node_center_x = node_center_x
            self.node_bottom = node_bottom

    def _place_step_block(
        self,
        step: WorkflowDefinitionStep,
        *,
        x: float,
        y: float,
    ) -> "_Placement":
        """Place a step's primary node and any nested children. Returns block extents."""
        node = self._add_node(step, x, y)
        node_center_x = x + _NODE_WIDTH / 2.0
        node_bottom = y + _NODE_HEIGHT

        if step.step_type == WorkflowDefinitionStepType.IF_ELSE:
            return self._place_branching_block(
                step,
                primary_node_bottom=node_bottom,
                primary_node_center_x=node_center_x,
                primary_x=x,
                branch_specs=[
                    ("Then", step.children, "#facc15"),
                    ("Else", step.else_children, "#fb923c"),
                ],
            )

        if step.step_type == WorkflowDefinitionStepType.TRY_CATCH:
            return self._place_branching_block(
                step,
                primary_node_bottom=node_bottom,
                primary_node_center_x=node_center_x,
                primary_x=x,
                branch_specs=[
                    ("Try", step.children, "#fb923c"),
                    ("Catch", step.else_children, "#f87171"),
                ],
            )

        if step.children:
            return self._place_grouped_children_block(
                step=step,
                node_bottom=node_bottom,
                node_center_x=node_center_x,
                node_x=x,
                label=self._group_label_for_step(step),
            )

        return WorkflowGraphView._Placement(
            right=x + _NODE_WIDTH,
            bottom=node_bottom,
            node_center_x=node_center_x,
            node_bottom=node_bottom,
        )

    def _place_grouped_children_block(
        self,
        *,
        step: WorkflowDefinitionStep,
        node_bottom: float,
        node_center_x: float,
        node_x: float,
        label: str,
    ) -> "_Placement":
        """Render children inside a labelled box under their parent."""
        group = _GroupBoxItem(label, fill_hex="#a855f7")
        self._scene.addItem(group)

        children_top = node_bottom + _VERTICAL_GAP
        children_x = node_x + _GROUP_PADDING_X
        cursor_y = children_top + _GROUP_PADDING_TOP
        max_right = node_x + _NODE_WIDTH
        previous_anchor: Optional[Tuple[float, float]] = (node_center_x, node_bottom)
        last_node_bottom = cursor_y

        for child in step.children:
            placement = self._place_step_block(child, x=children_x, y=cursor_y)
            child_top = cursor_y
            child_top_anchor = (placement.node_center_x, child_top)
            if previous_anchor is not None:
                self._draw_arrow(previous_anchor, child_top_anchor)
            previous_anchor = (placement.node_center_x, placement.node_bottom)
            last_node_bottom = placement.node_bottom
            cursor_y = placement.bottom + _VERTICAL_GAP
            if placement.right > max_right:
                max_right = placement.right

        group_height = max(cursor_y - children_top + _GROUP_PADDING_BOTTOM, _GROUP_PADDING_TOP + _GROUP_PADDING_BOTTOM)
        group_width = max(max_right - node_x, _NODE_WIDTH) + _GROUP_PADDING_X * 2.0 - _GROUP_PADDING_X
        group.set_geometry(node_x - _GROUP_PADDING_X, children_top, group_width + _GROUP_PADDING_X, group_height)

        return WorkflowGraphView._Placement(
            right=node_x - _GROUP_PADDING_X + group_width + _GROUP_PADDING_X,
            bottom=children_top + group_height,
            node_center_x=node_center_x,
            node_bottom=last_node_bottom,
        )

    def _place_branching_block(
        self,
        step: WorkflowDefinitionStep,
        *,
        primary_node_bottom: float,
        primary_node_center_x: float,
        primary_x: float,
        branch_specs: List[Tuple[str, List[WorkflowDefinitionStep], str]],
    ) -> "_Placement":
        """Place IF/ELSE or TRY/CATCH branches side-by-side beneath the primary node."""
        branches_top = primary_node_bottom + _VERTICAL_GAP
        cursor_x = primary_x
        max_bottom = branches_top
        max_right = primary_x + _NODE_WIDTH

        for label, children_list, color_hex in branch_specs:
            branch_top = branches_top
            branch_left = cursor_x
            inner_x = branch_left + _GROUP_PADDING_X
            inner_y = branch_top + _GROUP_PADDING_TOP
            inner_max_right = inner_x + _NODE_WIDTH
            previous_anchor: Optional[Tuple[float, float]] = None
            cursor_y = inner_y
            for child in (children_list or []):
                placement = self._place_step_block(child, x=inner_x, y=cursor_y)
                child_top_anchor = (placement.node_center_x, cursor_y)
                if previous_anchor is None:
                    self._draw_arrow((primary_node_center_x, primary_node_bottom), child_top_anchor)
                else:
                    self._draw_arrow(previous_anchor, child_top_anchor)
                previous_anchor = (placement.node_center_x, placement.node_bottom)
                cursor_y = placement.bottom + _VERTICAL_GAP
                if placement.right > inner_max_right:
                    inner_max_right = placement.right
            if not children_list:
                placeholder = QGraphicsSimpleTextItem(f"({label.lower()} branch is empty)")
                placeholder.setBrush(QBrush(QColor("#94a3b8")))
                placeholder.setPos(inner_x, inner_y)
                self._scene.addItem(placeholder)
                cursor_y = inner_y + 24.0

            group = _GroupBoxItem(label, fill_hex=color_hex)
            self._scene.addItem(group)
            group_height = max(cursor_y - branch_top + _GROUP_PADDING_BOTTOM, _GROUP_PADDING_TOP + _GROUP_PADDING_BOTTOM)
            group_width = max(inner_max_right - branch_left, _NODE_WIDTH) + _GROUP_PADDING_X
            group.set_geometry(branch_left, branch_top, group_width, group_height)

            branch_right = branch_left + group_width
            if branch_right > max_right:
                max_right = branch_right
            branch_bottom = branch_top + group_height
            if branch_bottom > max_bottom:
                max_bottom = branch_bottom
            cursor_x = branch_right + _BRANCH_HORIZONTAL_GAP

        return WorkflowGraphView._Placement(
            right=max_right,
            bottom=max_bottom,
            node_center_x=primary_node_center_x,
            node_bottom=primary_node_bottom,
        )

    def _add_node(self, step: WorkflowDefinitionStep, x: float, y: float) -> _StepNodeItem:
        node = _StepNodeItem(step, self)
        node.setPos(x, y)
        self._scene.addItem(node)
        self._nodes_by_step_id[step.id] = node
        return node

    def _draw_arrow(self, source: Tuple[float, float], target: Tuple[float, float]) -> None:
        """Connect two nodes with a vertical arrow."""
        pen = QPen(QColor("#cbd5e1"), 1.6)
        line = self._scene.addLine(source[0], source[1], target[0], target[1] - _ARROW_LENGTH, pen)
        line.setZValue(-1)
        # Arrowhead — small triangle pointing down at the target.
        arrow_brush = QBrush(QColor("#cbd5e1"))
        head = QGraphicsRectItem(0.0, 0.0, _ARROW_LENGTH, _ARROW_LENGTH)
        head.setPen(QPen(QColor("#cbd5e1"), 1.0))
        head.setBrush(arrow_brush)
        head.setPos(target[0] - _ARROW_LENGTH / 2.0, target[1] - _ARROW_LENGTH)
        head.setZValue(-1)
        self._scene.addItem(head)

    @staticmethod
    def _group_label_for_step(step: WorkflowDefinitionStep) -> str:
        if step.step_type == WorkflowDefinitionStepType.FOR_LOOP:
            loop_type = str((step.config or {}).get("loop_type", "range") or "range")
            return f"Loop body ({loop_type})"
        if step.step_type == WorkflowDefinitionStepType.SET_VARIABLE:
            return "After variable"
        return "Children"
