"""Progress, Tree, Text, Code, Diff, Separator, and ActionHint handles.

These handles have more complex state management than the simpler panel,
table, and status handles.
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any

from cleveragents.cli.output.handles._base import ElementHandle
from cleveragents.cli.output.handles._models import (
    MAX_TREE_DEPTH,
    ActionHint,
    CodeBlock,
    DiffBlock,
    DiffHunk,
    DiffLine,
    ProgressIndicator,
    ProgressStep,
    Separator,
    TextBlock,
    Tree,
    TreeNode,
)

if TYPE_CHECKING:
    from cleveragents.cli.output.session import OutputSession


# ---------------------------------------------------------------------------
# ProgressHandle
# ---------------------------------------------------------------------------


class ProgressHandle(ElementHandle[ProgressIndicator]):
    """Handle for updating a ProgressIndicator element.

    Progress handles are unique in that they receive many rapid updates.
    The materialisation strategy may throttle update events to avoid
    overwhelming the terminal (e.g. limiting redraws to 10/sec).
    """

    _MIN_UPDATE_INTERVAL: float = 0.1

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: ProgressIndicator,
    ) -> None:
        super().__init__(handle_id, "progress", declaration_index, session, element)
        self._last_emit_time: float = 0.0

    def _throttled_emit(self, update_type: str, delta: dict[str, Any]) -> None:
        """Emit an update event, throttled to _MIN_UPDATE_INTERVAL."""
        now = time.monotonic()
        if (now - self._last_emit_time) >= self._MIN_UPDATE_INTERVAL:
            self._last_emit_time = now
            self._emit_update(update_type, delta)

    def set_progress(self, current: int, total: int | None = None) -> None:
        """Update the progress counter.

        P1-1 fix (Luis review): validates *total* is non-negative when
        provided, preventing nonsensical percentages in renderers.
        """
        if current < 0:
            raise ValueError("current must be non-negative")
        if total is not None and total < 0:
            raise ValueError("total must be non-negative")
        with self._lock:
            self._require_open()
            self._element.current = current
            if total is not None:
                self._element.total = total
            self._throttled_emit(
                "progress_changed",
                {"current": current, "total": self._element.total},
            )

    def set_label(self, label: str) -> None:
        """Update the progress label text."""
        if not label:
            raise ValueError("label cannot be empty")
        with self._lock:
            self._require_open()
            self._element.label = label
            self._emit_update("label_changed", {"label": label})

    def tick(self) -> None:
        """Increment progress by 1."""
        with self._lock:
            self._require_open()
            if self._element.current is None:
                self._element.current = 1
            else:
                self._element.current += 1
            self._throttled_emit(
                "progress_changed",
                {"current": self._element.current, "total": self._element.total},
            )

    def increment(self, delta: int = 1) -> None:
        """Increment progress by *delta*.

        P3-8 fix (Luis review): rejects zero delta as a likely caller
        error (zero-delta makes no progress).
        """
        if delta <= 0:
            raise ValueError("delta must be positive")
        with self._lock:
            self._require_open()
            if self._element.current is None:
                self._element.current = delta
            else:
                self._element.current += delta
            self._throttled_emit(
                "progress_changed",
                {"current": self._element.current, "total": self._element.total},
            )

    def set_indeterminate(self) -> None:
        """Switch to indeterminate (spinner) mode."""
        with self._lock:
            self._require_open()
            self._element.indeterminate = True
            self._emit_update("indeterminate", {})

    def set_step_status(self, step_label: str, status: str) -> None:
        """Update the status of a named step."""
        if not step_label:
            raise ValueError("step_label cannot be empty")
        valid_statuses = {"pending", "active", "done", "error", "skipped"}
        if status not in valid_statuses:
            raise ValueError(
                f"status must be one of {valid_statuses!r}, got {status!r}"
            )
        with self._lock:
            self._require_open()
            if self._element.steps is None:
                self._element.steps = []
            for step in self._element.steps:
                if step.label == step_label:
                    step.status = status
                    break
            else:
                self._element.steps.append(
                    ProgressStep(label=step_label, status=status)
                )
            self._emit_update(
                "step_status_changed",
                {"step_label": step_label, "status": status},
            )


# ---------------------------------------------------------------------------
# TreeHandle
# ---------------------------------------------------------------------------


class TreeHandle(ElementHandle[Tree]):
    """Handle for building a Tree element incrementally.

    The tree is accessed via path strings using ``/`` as separator,
    starting from the root node's label.
    """

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: Tree,
    ) -> None:
        super().__init__(handle_id, "tree", declaration_index, session, element)
        self._path_cache: dict[str, TreeNode] = {
            element.root.label: element.root,
            "": element.root,
        }

    def _find_node(self, path: str) -> TreeNode | None:
        """Locate a node by its ``/``-separated path using the path cache."""
        cached = self._path_cache.get(path)
        if cached is not None:
            return cached
        parts = [p for p in path.split("/") if p]
        if not parts:
            return self._element.root
        if parts[0] != self._element.root.label:
            return None
        node = self._element.root
        for part in parts[1:]:
            found = False
            for child in node.children:
                if child.label == part:
                    node = child
                    found = True
                    break
            if not found:
                return None
        self._path_cache[path] = node
        return node

    @staticmethod
    def _node_depth(path: str) -> int:
        """Return the nesting depth implied by a ``/``-separated path."""
        parts = [p for p in path.split("/") if p]
        return len(parts)

    def add_child(
        self,
        parent_path: str,
        label: str,
        *,
        style_hint: str | None = None,
        collapsed: bool = False,
        metadata: dict[str, Any] | None = None,
    ) -> str:
        """Add a child node under *parent_path* and return the full path."""
        if not label:
            raise ValueError("label cannot be empty")
        if "/" in label:
            raise ValueError(f"Tree node label cannot contain '/': {label!r}")
        with self._lock:
            self._require_open()
            parent = self._find_node(parent_path)
            if parent is None:
                raise ValueError(f"Parent path not found: {parent_path!r}")
            normalized_path = parent_path if parent_path else self._element.root.label
            child_depth = self._node_depth(normalized_path) + 1
            if child_depth > MAX_TREE_DEPTH:
                raise ValueError(f"Maximum tree depth ({MAX_TREE_DEPTH}) exceeded")
            child = TreeNode(
                label=label,
                style_hint=style_hint,
                collapsed=collapsed,
                metadata=metadata or {},
            )
            parent.children.append(child)
            if parent_path:
                full_path = f"{parent_path}/{label}"
            else:
                full_path = f"{self._element.root.label}/{label}"
            self._path_cache[full_path] = child
            self._emit_update("child_added", {"parent": parent_path, "label": label})
            return full_path

    def set_node_style(self, path: str, style_hint: str) -> None:
        """Update the style hint on an existing node."""
        if not path:
            raise ValueError("path cannot be empty")
        with self._lock:
            self._require_open()
            node = self._find_node(path)
            if node is None:
                raise ValueError(f"Node path not found: {path!r}")
            node.style_hint = style_hint
            self._emit_update("node_style_changed", {"path": path})


# ---------------------------------------------------------------------------
# TextHandle
# ---------------------------------------------------------------------------


class TextHandle(ElementHandle[TextBlock]):
    """Handle for a free-form text block.

    Text content is buffered in a ``list[str]`` and joined lazily to
    avoid O(N^2) string concatenation on repeated ``append()`` calls
    (F2 fix).
    """

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: TextBlock,
    ) -> None:
        super().__init__(handle_id, "text", declaration_index, session, element)
        self._text_parts: list[str] = [element.content] if element.content else []

    def _flush_buffer(self) -> None:
        """Synchronise the element's content from the internal buffer."""
        self._element.content = "".join(self._text_parts)

    @property
    def element(self) -> TextBlock:
        """The accumulated element state with buffered text flushed."""
        with self._lock:
            self._flush_buffer()
            return self._element

    def element_copy(self) -> TextBlock:
        """Return a deep copy with buffered text flushed."""
        with self._lock:
            self._flush_buffer()
            return self._element.model_copy(deep=True)

    def _emit_update(self, update_type: str, delta: dict[str, Any]) -> None:
        """Flush the text buffer before emitting a snapshot.

        Short-circuits before flushing when the strategy does not consume
        incremental updates, preserving the lazy-buffer O(1) amortised
        cost of ``append()`` (R2-C2 fix).
        """
        if not self._session._strategy.supports_incremental_updates:
            return
        self._flush_buffer()
        super()._emit_update(update_type, delta)

    def append(self, text: str) -> None:
        """Append text to the current content."""
        if text is None:
            raise ValueError("text cannot be None")
        with self._lock:
            self._require_open()
            self._text_parts.append(text)
            self._emit_update("text_appended", {"text": text})

    def set_content(self, content: str) -> None:
        """Replace the entire content."""
        if content is None:
            raise ValueError("content cannot be None")
        with self._lock:
            self._require_open()
            self._text_parts = [content]
            self._element.content = content
            self._emit_update("content_set", {"content": content})

    def close(self) -> None:
        """Close the text handle after flushing buffered content.

        P3-2 fix (Luis review): flush the buffer then delegate to the
        base class ``close()`` instead of reimplementing the lifecycle
        logic.  This ensures TextHandle tracks any future base-class
        changes (logging, metrics, etc.).
        """
        with self._lock:
            if self._state == "open":
                self._flush_buffer()
        super().close()


# ---------------------------------------------------------------------------
# CodeHandle
# ---------------------------------------------------------------------------


class CodeHandle(ElementHandle[CodeBlock]):
    """Handle for a source-code block with syntax highlighting hints."""

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: CodeBlock,
    ) -> None:
        super().__init__(handle_id, "code", declaration_index, session, element)

    def set_content(self, content: str) -> None:
        """Replace the code content."""
        if content is None:
            raise ValueError("content cannot be None")
        with self._lock:
            self._require_open()
            self._element.content = content
            self._emit_update("content_set", {"content": content})

    def set_language(self, language: str) -> None:
        """Set or change the language hint."""
        if not language:
            raise ValueError("language cannot be empty")
        with self._lock:
            self._require_open()
            self._element.language = language
            self._emit_update("language_set", {"language": language})

    def set_highlight_lines(self, lines: list[int]) -> None:
        """Set lines to highlight."""
        if lines is None:
            raise ValueError("lines cannot be None")
        with self._lock:
            self._require_open()
            self._element.highlight_lines = list(lines)
            self._emit_update("highlight_lines_set", {"lines": lines})


# ---------------------------------------------------------------------------
# DiffHandle
# ---------------------------------------------------------------------------


class DiffHandle(ElementHandle[DiffBlock]):
    """Handle for a unified-diff display element."""

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: DiffBlock,
    ) -> None:
        super().__init__(handle_id, "diff", declaration_index, session, element)

    def add_hunk(self, header: str, lines: list[DiffLine]) -> None:
        """Add a diff hunk."""
        if not header:
            raise ValueError("header cannot be empty")
        if lines is None:
            raise ValueError("lines cannot be None")
        with self._lock:
            self._require_open()
            hunk = DiffHunk(header=header, lines=list(lines))
            self._element.hunks.append(hunk)
            self._emit_update("hunk_added", {"header": header})

    def set_stats(
        self,
        insertions: int,
        deletions: int,
        **extra: Any,
    ) -> None:
        """Set diff statistics."""
        if insertions < 0:
            raise ValueError("insertions must be non-negative")
        if deletions < 0:
            raise ValueError("deletions must be non-negative")
        with self._lock:
            self._require_open()
            self._element.stats = {
                "insertions": insertions,
                "deletions": deletions,
                **extra,
            }
            self._emit_update(
                "stats_set",
                {"insertions": insertions, "deletions": deletions},
            )


# ---------------------------------------------------------------------------
# SeparatorHandle / ActionHintHandle
# ---------------------------------------------------------------------------


class SeparatorHandle(ElementHandle[Separator]):
    """Handle for a visual separator between elements (auto-closed)."""

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: Separator,
    ) -> None:
        super().__init__(handle_id, "separator", declaration_index, session, element)


class ActionHintHandle(ElementHandle[ActionHint]):
    """Handle for suggested next-step CLI commands (auto-closed)."""

    def __init__(
        self,
        handle_id: str,
        declaration_index: int,
        session: OutputSession,
        element: ActionHint,
    ) -> None:
        super().__init__(handle_id, "action_hint", declaration_index, session, element)
