"""Renderer architecture for CLI output formatting.

This module implements the formatter/renderer separation:
- Data structures: ToolCallData, ToolOutputData (frozen dataclasses)
- Protocols: ToolCallRenderer, ToolOutputRenderer
- Implementations: Rich* (with markup) and Plain* (without markup)
- Factory: get_renderers() for TTY-based selection

Usage:
    from terminaluse.lib.utils.output.renderers import (
        ToolCallData,
        ToolOutputData,
        get_renderers,
    )

    # Get appropriate renderers based on TTY
    call_renderer, output_renderer = get_renderers(console)

    # Render structured data
    data = ToolCallData(tool_name="read_file", args={"path": "/foo"})
    output = call_renderer.render(data)
"""

from __future__ import annotations

import json
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable

# =============================================================================
# Data Structures
# =============================================================================


@dataclass(frozen=True, slots=True)
class ToolCallData:
    """Structured tool call data, decoupled from rendering.

    Attributes:
        tool_name: Name of the tool being called
        args: Tool arguments as key-value mapping
        multiline_args: Subset of args that contain multiline values
    """

    tool_name: str
    args: Mapping[str, Any]
    multiline_args: Mapping[str, str] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class ToolOutputData:
    """Structured tool output data, decoupled from rendering.

    Attributes:
        lines: Output lines (may be truncated)
        total_line_count: Original line count before truncation
        tool_name: Optional tool name for tool-specific rendering
    """

    lines: Sequence[str]
    total_line_count: int
    tool_name: str | None = None

    @property
    def is_empty(self) -> bool:
        """Returns True if output has no content."""
        return self.total_line_count == 0


# =============================================================================
# Protocols
# =============================================================================


@runtime_checkable
class ToolCallRenderer(Protocol):
    """Protocol for rendering tool call data."""

    verbose: bool

    def render(self, data: ToolCallData) -> str:
        """Render tool call data to string."""
        ...


@runtime_checkable
class ToolOutputRenderer(Protocol):
    """Protocol for rendering tool output data."""

    verbose: bool

    def render(self, data: ToolOutputData) -> str:
        """Render tool output data to string."""
        ...


# =============================================================================
# Rich Renderers (with markup)
# =============================================================================


@dataclass(frozen=True, slots=True)
class RichToolCallRenderer:
    """Renders tool calls with Rich markup for TTY output."""

    verbose: bool = False
    max_value_length: int = 70

    def render(self, data: ToolCallData) -> str:
        """Render tool call with Rich markup."""
        lines = [f"\n[yellow bold][{data.tool_name}][/yellow bold]"]

        for key, value in data.args.items():
            formatted = self._format_arg(key, value, data.multiline_args)
            lines.append(formatted)

        return "\n".join(lines)

    def _format_arg(self, key: str, value: Any, multiline_args: Mapping[str, str]) -> str:
        """Format a single argument."""
        if value is None:
            return f"  | {key}: null"

        if isinstance(value, str):
            return self._format_string_arg(key, value, key in multiline_args)

        # Non-string: JSON serialize
        display = json.dumps(value)
        if len(display) > self.max_value_length and not self.verbose:
            display = display[: self.max_value_length - 3] + "..."
        return f"  | {key}: {display}"

    def _format_string_arg(self, key: str, value: str, is_multiline: bool) -> str:
        """Format a string argument."""
        if is_multiline or len(value) > self.max_value_length:
            if self.verbose:
                lines = [f"  | {key}:"]
                for line in value.split("\n"):
                    lines.append(f"  |   {line}")
                return "\n".join(lines)
            else:
                display = value.replace("\n", " ")
                if len(display) > self.max_value_length:
                    display = display[: self.max_value_length - 3] + "..."
                return f"  | {key}: {display}"

        return f"  | {key}: {value}"


@dataclass(frozen=True, slots=True)
class RichToolOutputRenderer:
    """Renders tool output with Rich markup for TTY output."""

    verbose: bool = False
    max_lines: int = 15

    def render(self, data: ToolOutputData) -> str:
        """Render tool output with Rich markup."""
        if data.is_empty:
            return "\n[green dim]  '--> (empty)[/green dim]\n"

        if len(data.lines) == 1:
            return f"\n[green]  '--> {data.lines[0]}[/green]\n"

        # Multiple lines
        result_lines = ["\n[green]  '-->[/green]"]

        lines_to_show = data.lines if self.verbose else data.lines[: self.max_lines]
        for line in lines_to_show:
            result_lines.append(f"       {line}")

        # Show truncation message if applicable
        if not self.verbose and data.total_line_count > len(data.lines):
            remaining = data.total_line_count - len(data.lines)
            result_lines.append(f"[dim italic]       ... ({remaining} more lines)[/dim italic]")
        elif not self.verbose and len(data.lines) > self.max_lines:
            remaining = len(data.lines) - self.max_lines
            result_lines.append(f"[dim italic]       ... ({remaining} more lines)[/dim italic]")

        return "\n".join(result_lines) + "\n"


# =============================================================================
# Plain Text Renderers (no markup)
# =============================================================================


@dataclass(frozen=True, slots=True)
class PlainToolCallRenderer:
    """Renders tool calls as plain text for non-TTY output."""

    verbose: bool = False
    max_value_length: int = 70

    def render(self, data: ToolCallData) -> str:
        """Render tool call as plain text."""
        lines = [f"\n[{data.tool_name}]"]

        for key, value in data.args.items():
            formatted = self._format_arg(key, value, data.multiline_args)
            lines.append(formatted)

        return "\n".join(lines)

    def _format_arg(self, key: str, value: Any, multiline_args: Mapping[str, str]) -> str:
        """Format a single argument."""
        if value is None:
            return f"  | {key}: null"

        if isinstance(value, str):
            return self._format_string_arg(key, value, key in multiline_args)

        display = json.dumps(value)
        if len(display) > self.max_value_length and not self.verbose:
            display = display[: self.max_value_length - 3] + "..."
        return f"  | {key}: {display}"

    def _format_string_arg(self, key: str, value: str, is_multiline: bool) -> str:
        """Format a string argument."""
        if is_multiline or len(value) > self.max_value_length:
            if self.verbose:
                lines = [f"  | {key}:"]
                for line in value.split("\n"):
                    lines.append(f"  |   {line}")
                return "\n".join(lines)
            else:
                display = value.replace("\n", " ")
                if len(display) > self.max_value_length:
                    display = display[: self.max_value_length - 3] + "..."
                return f"  | {key}: {display}"

        return f"  | {key}: {value}"


@dataclass(frozen=True, slots=True)
class PlainToolOutputRenderer:
    """Renders tool output as plain text for non-TTY output."""

    verbose: bool = False
    max_lines: int = 15

    def render(self, data: ToolOutputData) -> str:
        """Render tool output as plain text."""
        if data.is_empty:
            return "\n  '--> (empty)\n"

        if len(data.lines) == 1:
            return f"\n  '--> {data.lines[0]}\n"

        # Multiple lines
        result_lines = ["\n  '-->"]

        lines_to_show = data.lines if self.verbose else data.lines[: self.max_lines]
        for line in lines_to_show:
            result_lines.append(f"       {line}")

        # Show truncation message if applicable
        if not self.verbose and data.total_line_count > len(data.lines):
            remaining = data.total_line_count - len(data.lines)
            result_lines.append(f"       ... ({remaining} more lines)")
        elif not self.verbose and len(data.lines) > self.max_lines:
            remaining = len(data.lines) - self.max_lines
            result_lines.append(f"       ... ({remaining} more lines)")

        return "\n".join(result_lines) + "\n"


# =============================================================================
# Extractor (for formatter compatibility)
# =============================================================================


@dataclass(slots=True)
class ToolRequestExtractor:
    """Extracts structured data from tool requests."""

    def extract(self, tool_name: str, input_json: str) -> ToolCallData:
        """Extract ToolCallData from raw input."""
        try:
            args: dict[str, Any] = json.loads(input_json) if input_json else {}
        except json.JSONDecodeError:
            args = {"_raw": input_json}

        multiline = {k: v for k, v in args.items() if isinstance(v, str) and "\n" in v}

        return ToolCallData(
            tool_name=tool_name,
            args=args,
            multiline_args=multiline,
        )


# =============================================================================
# Factory
# =============================================================================


def get_renderers(
    console: Any,
    verbose: bool = False,
    max_lines: int = 15,
    max_value_length: int = 70,
) -> tuple[ToolCallRenderer, ToolOutputRenderer]:
    """Get appropriate renderers based on console TTY status.

    Args:
        console: Rich Console instance (or mock with is_terminal attribute)
        verbose: Whether to show full output without truncation
        max_lines: Maximum lines to show in non-verbose mode
        max_value_length: Maximum value length before truncation

    Returns:
        Tuple of (ToolCallRenderer, ToolOutputRenderer)
    """
    if console.is_terminal:
        return (
            RichToolCallRenderer(verbose=verbose, max_value_length=max_value_length),
            RichToolOutputRenderer(verbose=verbose, max_lines=max_lines),
        )
    else:
        return (
            PlainToolCallRenderer(verbose=verbose, max_value_length=max_value_length),
            PlainToolOutputRenderer(verbose=verbose, max_lines=max_lines),
        )
