"""Message Display Service for LLM responses.

Handles unified message display coordination, eliminating duplicated
display logic throughout the LLM service. Follows KISS principle with
single responsibility for message display orchestration.
"""

import logging
from typing import Any, Dict, List, Optional, Tuple

from core.io.visual_effects import ColorPalette

logger = logging.getLogger(__name__)


class MessageDisplayService:
    """Unified service for coordinating LLM message display.
    
    Eliminates code duplication by providing a single point of control
    for all message display operations including thinking duration,
    assistant responses, and tool execution results.
    
    Follows KISS principle: Single responsibility for message display coordination.
    Implements DRY principle: Eliminates ~90 lines of duplicated display code.
    """
    
    def __init__(self, renderer):
        """Initialize message display service.

        Args:
            renderer: Terminal renderer with message_coordinator
        """
        self.renderer = renderer
        self.message_coordinator = renderer.message_coordinator
        self._streaming_active = False

        logger.info("Message display service initialized")
    
    
    def display_thinking_and_response(self,
                                    thinking_duration: float,
                                    response: str,
                                    show_thinking_threshold: float = 0.1) -> None:
        """Display thinking duration and assistant response atomically.

        Args:
            thinking_duration: Time spent thinking in seconds
            response: Assistant response content
            show_thinking_threshold: Minimum duration to show thinking message
        """
        # Use the unified display method for consistency
        self.display_complete_response(
            thinking_duration=thinking_duration,
            response=response,
            tool_results=None,
            original_tools=None,
            show_thinking_threshold=show_thinking_threshold
        )
    
    def display_tool_results(self, tool_results: List[Any], original_tools: List[Dict] = None) -> None:
        """Display tool execution results with consistent formatting.

        Args:
            tool_results: List of tool execution result objects
            original_tools: List of original tool data for command extraction
        """
        for i, result in enumerate(tool_results):
            # Get original tool data for display
            tool_data = original_tools[i] if original_tools and i < len(original_tools) else {}

            # Extract tool name and args for modern rendering
            tool_name, tool_args = self._extract_tool_name_args(result, tool_data)

            # Get result summary for inline display
            result_summary = self._get_result_summary_modern(result, tool_data)

            # Determine status
            tool_status = "error" if not result.success else "success"

            # For file operations, we show summary inline and never show raw output
            # For other tools, we might show output if it's short enough
            output_content = ""
            show_output = self._should_show_output(result)

            # Don't show output for file_read/file_edit - summary is enough
            if result.tool_type in ("file_read", "file_edit"):
                show_output = False

            if show_output:
                output_lines = self._format_tool_output(result)
                output_content = "\n".join(output_lines)

            # Create message sequence for this tool using modern "tool" type
            tool_messages = [
                ("tool", output_content, {
                    "tool_name": tool_name,
                    "tool_args": tool_args,
                    "tool_status": tool_status,
                    "result_summary": result_summary
                })
            ]

            # No spacing between tools - boxes provide visual separation

            # Display tool messages using coordinator
            self.message_coordinator.display_message_sequence(tool_messages)

        logger.debug(f"Displayed {len(tool_results)} tool results")
    
    def display_user_message(self, message: str) -> None:
        """Display user message through coordinator.

        Args:
            message: User's input message
        """
        # Don't display user messages in pipe mode
        if getattr(self.renderer, 'pipe_mode', False):
            logger.debug(f"Suppressing user message in pipe mode: {len(message)} chars")
            return

        message_sequence = [("user", message, {})]
        self.message_coordinator.display_message_sequence(message_sequence)
        logger.debug(f"Displayed user message: {len(message)} chars")
    
    def display_system_message(self, message: str) -> None:
        """Display system message through coordinator.
        
        Args:
            message: System message to display
        """
        message_sequence = [("system", message, {})]
        self.message_coordinator.display_message_sequence(message_sequence)
        logger.debug(f"Displayed system message: {message[:50]}...")
    
    def display_error_message(self, error: str) -> None:
        """Display error message through coordinator.
        
        Args:
            error: Error message to display
        """
        message_sequence = [("error", f"Error: {error}", {})]
        self.message_coordinator.display_message_sequence(message_sequence)
        logger.debug(f"Displayed error message: {error[:50]}...")
    
    def display_cancellation_message(self) -> None:
        """Display request cancellation message."""
        # Don't display cancellation message in pipe mode (it's expected during cleanup)
        pipe_mode = getattr(self.renderer, 'pipe_mode', False)

        if hasattr(self.renderer, 'pipe_mode') and pipe_mode:
            logger.debug("Suppressing cancellation message in pipe mode")
            return

        message_sequence = [
            ("system", "Request cancelled", {})
        ]
        self.message_coordinator.display_message_sequence(message_sequence)
        logger.debug("Displayed cancellation message")
    
    def _format_tool_header(self, result, tool_data: Dict = None) -> str:
        """Format tool execution header with consistent styling.

        Args:
            result: Tool execution result
            tool_data: Original tool data for command/name extraction

        Returns:
            Formatted tool header string
        """
        # Tool indicator with dynamic color support
        indicator = f"{ColorPalette.BRIGHT_LIME}⏺{ColorPalette.RESET}"

        if result.tool_type == "terminal":
            # Extract actual command from original tool data
            command = tool_data.get("command", "unknown") if tool_data else result.tool_id
            return f"{indicator} terminal({self._truncate_tool_args(command)})"
        elif result.tool_type == "mcp_tool":
            # Extract tool name and arguments from original tool data
            tool_name = tool_data.get("name", "unknown") if tool_data else result.tool_id
            arguments = tool_data.get("arguments", {}) if tool_data else {}

            # Clean up malformed tool names (may contain XML from confused LLM)
            if '<' in tool_name or '>' in tool_name:
                import re
                # Try to extract clean tool name
                match = re.search(r'<tool_call>([^<]+)', tool_name)
                if match:
                    tool_name = match.group(1).strip()
                else:
                    # Find last word that looks like a tool name
                    words = re.findall(r'\b([a-z_]+)\b', tool_name.lower())
                    tool_name = words[-1] if words else "mcp_tool"

            # For Read-like tools, show file path with line info
            if tool_name.lower() in ("read", "file_read", "readfile"):
                file_path = arguments.get("file_path") or arguments.get("path") or arguments.get("file", "")
                offset = arguments.get("offset")
                limit = arguments.get("limit")
                truncated_path = self._truncate_tool_args(file_path)
                if offset is not None or limit is not None:
                    offset_val = offset if offset is not None else 0
                    if limit:
                        return f"{indicator} {tool_name}({truncated_path}, lines {offset_val + 1}-{offset_val + limit})"
                    else:
                        return f"{indicator} {tool_name}({truncated_path}, from line {offset_val + 1})"
                return f"{indicator} {tool_name}({truncated_path})"

            # Format arguments cleanly
            if arguments:
                # Show key arguments inline, truncate long values
                arg_parts = []
                for k, v in list(arguments.items())[:3]:  # Max 3 args
                    v_str = str(v)
                    if len(v_str) > 30:
                        v_str = v_str[:27] + "..."
                    arg_parts.append(f'{k}="{v_str}"')
                args_display = ", ".join(arg_parts)
                if len(arguments) > 3:
                    args_display += f", +{len(arguments) - 3} more"
                return f"{indicator} {tool_name}({self._truncate_tool_args(args_display)})"
            else:
                return f"{indicator} {tool_name}()"
        elif result.tool_type.startswith("file_"):
            # Extract filename/path from file operation data
            display_info = self._extract_file_display_info(tool_data, result.tool_type)
            return f"{indicator} {result.tool_type}({self._truncate_tool_args(display_info)})"
        else:
            return f"{indicator} {result.tool_type}({self._truncate_tool_args(result.tool_id)})"

    def _extract_file_display_info(self, tool_data: Dict, tool_type: str) -> str:
        """Extract display information from file operation data.

        Args:
            tool_data: Original tool data
            tool_type: Type of file operation

        Returns:
            Filename or path to display
        """
        if not tool_data:
            return "unknown"

        # Most file operations use 'file' key
        if "file" in tool_data:
            return tool_data["file"]

        # Move/copy operations use 'from' and 'to'
        if "from" in tool_data and "to" in tool_data:
            return f"{tool_data['from']} → {tool_data['to']}"

        # mkdir/rmdir use 'path'
        if "path" in tool_data:
            return tool_data["path"]

        return "unknown"
    
    def _format_tool_result(self, result, tool_data: Dict = None) -> str:
        """Format tool execution result summary.

        Args:
            result: Tool execution result
            tool_data: Original tool data for request info (optional)

        Returns:
            Formatted result summary string
        """
        if result.success:
            # Count output characteristics for summary
            output_lines = result.output.count('\n') + 1 if result.output else 0
            output_chars = len(result.output) if result.output else 0

            if result.tool_type == "terminal" and result.output:
                return f"\033[32m ▮ Read {output_lines} lines ({output_chars} chars)\033[0m"
            elif result.tool_type == "file_read" and result.output:
                # Extract line count and optional range from output
                # Format: "✓ Read X lines from path (lines N-M):" or "✓ Read X lines from path:"
                import re
                match = re.search(r'Read (\d+) lines from .+?(?:\(lines ([^)]+)\))?:', result.output)
                if match:
                    line_count = match.group(1)
                    lines_from_output = match.group(2)  # May be None

                    # Build line range from various sources
                    lines_spec = None
                    if lines_from_output:
                        lines_spec = lines_from_output
                    elif tool_data:
                        if tool_data.get("lines"):
                            lines_spec = tool_data["lines"]
                        elif tool_data.get("offset") is not None or tool_data.get("limit") is not None:
                            # Calculate range from offset/limit
                            offset = tool_data.get("offset", 0)
                            limit = tool_data.get("limit")
                            start = offset + 1  # 1-indexed for display
                            if limit:
                                lines_spec = f"{start}-{start + int(line_count) - 1}"
                            else:
                                lines_spec = f"{start}+"

                    if lines_spec:
                        return f"\033[32m ▮ Read {line_count} lines (lines {lines_spec})\033[0m"
                    return f"\033[32m ▮ Read {line_count} lines\033[0m"
                return f"\033[32m ▮ Success\033[0m"
            elif result.tool_type == "mcp_tool" and result.output:
                # Try to summarize JSON output
                try:
                    import json
                    data = json.loads(result.output)
                    if isinstance(data, dict):
                        # Count items in response
                        if "content" in data:
                            content = data["content"]
                            if isinstance(content, list):
                                return f"\033[32m ▮ Returned {len(content)} items\033[0m"
                            elif isinstance(content, str):
                                preview = content[:40] + "..." if len(content) > 40 else content
                                return f"\033[32m ▮ {preview}\033[0m"
                        # Count top-level keys
                        keys = list(data.keys())[:3]
                        return f"\033[32m ▮ Returned {{{', '.join(keys)}{'...' if len(data) > 3 else ''}}}\033[0m"
                    elif isinstance(data, list):
                        return f"\033[32m ▮ Returned {len(data)} items\033[0m"
                except (json.JSONDecodeError, TypeError):
                    pass
                # Fallback to text preview
                preview = result.output[:50].replace('\n', ' ')
                if len(result.output) > 50:
                    preview += "..."
                return f"\033[32m ▮ {preview}\033[0m"
            else:
                return f"\033[32m ▮ Success\033[0m"
        else:
            return f"\033[31m ▮ Error: {result.error}\033[0m"

    def _extract_tool_info(self, result, tool_data: Dict = None) -> tuple:
        """Extract tool name and arguments for modern rendering.

        Args:
            result: Tool execution result
            tool_data: Original tool data

        Returns:
            Tuple of (tool_name, tool_args)
        """
        if result.tool_type == "terminal":
            command = tool_data.get("command", "unknown") if tool_data else result.tool_id
            return ("terminal", self._truncate_tool_args(command))

        elif result.tool_type == "mcp_tool":
            tool_name = tool_data.get("name", "unknown") if tool_data else result.tool_id
            arguments = tool_data.get("arguments", {}) if tool_data else {}

            # Clean up malformed tool names
            if '<' in tool_name or '>' in tool_name:
                import re
                match = re.search(r'<tool_call>([^<]+)', tool_name)
                if match:
                    tool_name = match.group(1).strip()
                else:
                    words = re.findall(r'\b([a-z_]+)\b', tool_name.lower())
                    tool_name = words[-1] if words else "mcp_tool"

            # Format arguments as readable string
            if arguments:
                # For file read, show just the path
                if tool_name.lower() in ("read", "file_read", "readfile"):
                    file_path = arguments.get("file_path") or arguments.get("path") or ""
                    return (tool_name, self._truncate_tool_args(file_path))
                # For other tools, show key args
                arg_parts = []
                for k, v in list(arguments.items())[:2]:
                    if isinstance(v, str) and len(v) > 30:
                        v = v[:30] + "..."
                    arg_parts.append(f"{k}={v}")
                args_str = ", ".join(arg_parts)
                return (tool_name, self._truncate_tool_args(args_str))

            return (tool_name, "")

        elif result.tool_type == "file_read":
            # Response parser uses "file" as key, not "file_path"
            file_path = tool_data.get("file", "") if tool_data else ""
            return ("file_read", self._truncate_tool_args(file_path))

        elif result.tool_type == "file_edit":
            # Response parser uses "file" as key, not "file_path"
            file_path = tool_data.get("file", "") if tool_data else ""
            return ("file_edit", self._truncate_tool_args(file_path))

        return (result.tool_type or "tool", self._truncate_tool_args(result.tool_id or ""))

    def _extract_tool_name_args(self, result, tool_data: Dict = None):
        """Extract tool name and arguments for modern rendering.

        Args:
            result: Tool execution result
            tool_data: Original tool data

        Returns:
            Tuple of (tool_name, tool_args_string)
        """
        if result.tool_type == "terminal":
            command = tool_data.get("command", "unknown") if tool_data else result.tool_id
            return ("terminal", self._truncate_tool_args(command))

        elif result.tool_type == "mcp_tool":
            tool_name = tool_data.get("name", "unknown") if tool_data else result.tool_id
            arguments = tool_data.get("arguments", {}) if tool_data else {}

            # Clean up malformed tool names
            if '<' in tool_name or '>' in tool_name:
                import re
                match = re.search(r'<tool_call>([^<]+)', tool_name)
                if match:
                    tool_name = match.group(1).strip()
                else:
                    words = re.findall(r'\b([a-z_]+)\b', tool_name.lower())
                    tool_name = words[-1] if words else "mcp_tool"

            # Format arguments
            if tool_name.lower() in ("read", "file_read", "readfile"):
                file_path = arguments.get("file_path") or arguments.get("path") or arguments.get("file", "")
                return (tool_name, self._truncate_tool_args(file_path))
            elif arguments:
                arg_parts = []
                for k, v in list(arguments.items())[:2]:
                    v_str = str(v)
                    if len(v_str) > 40:
                        v_str = v_str[:37] + "..."
                    arg_parts.append(f'{k}="{v_str}"')
                args_str = ", ".join(arg_parts)
                return (tool_name, self._truncate_tool_args(args_str))
            else:
                return (tool_name, "")

        elif result.tool_type.startswith("file_"):
            display_info = self._extract_file_display_info(tool_data, result.tool_type)
            return (result.tool_type, self._truncate_tool_args(display_info))

        else:
            return (result.tool_type or "tool", self._truncate_tool_args(result.tool_id or ""))

    def _truncate_tool_args(self, args: str, max_length: int = 60) -> str:
        """Truncate tool arguments for single-line display.

        Args:
            args: Raw argument string (may contain newlines)
            max_length: Maximum length before truncation

        Returns:
            Truncated string with ellipsis if needed
        """
        # If multi-line, take first line only
        if '\n' in args:
            first_line = args.split('\n')[0]
            return f"{first_line}..."

        # If single line but too long, truncate
        if len(args) > max_length:
            return f"{args[:max_length]}..."

        return args

    def _get_result_summary_modern(self, result, tool_data: Dict = None) -> str:
        """Get result summary for inline display in modern tool rendering.

        Args:
            result: Tool execution result
            tool_data: Original tool data

        Returns:
            Summary string for inline display
        """
        return self._get_tool_result_summary(result, tool_data)

    def _get_tool_result_summary(self, result, tool_data: Dict = None) -> str:
        """Get a clean summary of tool result without ANSI codes.

        Args:
            result: Tool execution result
            tool_data: Original tool data

        Returns:
            Clean summary string
        """
        if result.success:
            output_lines = result.output.count('\n') + 1 if result.output else 0
            output_chars = len(result.output) if result.output else 0

            if result.tool_type == "terminal" and result.output:
                return f"Read {output_lines} lines ({output_chars} chars)"

            elif result.tool_type == "file_read" and result.output:
                import re
                match = re.search(r'Read (\d+) lines', result.output)
                if match:
                    return f"Read {match.group(1)} lines"
                return "Success"

            elif result.tool_type == "mcp_tool" and result.output:
                return f"Returned {output_chars} chars"

            elif result.tool_type == "file_edit":
                return "File updated"

            return "Success"
        else:
            return f"Error: {result.error}"

    def _should_show_output(self, result) -> bool:
        """Determine if tool output should be displayed inline.
        
        Args:
            result: Tool execution result
            
        Returns:
            True if output should be shown
        """
        return (result.success and 
                result.output and 
                len(result.output) < 500)
    
    def _format_tool_output(self, result) -> List[str]:
        """Format tool output for inline display.

        Args:
            result: Tool execution result

        Returns:
            List of formatted output lines
        """
        # Special formatting for file_edit with diff info
        if (result.tool_type == "file_edit" and
            hasattr(result, 'metadata') and
            result.metadata and
            'diff_info' in result.metadata):
            return self._format_edit_diff(result)

        # Default formatting for other outputs
        output_lines = result.output.strip().split('\n')
        formatted_lines = []

        # Show first 20 lines with indentation
        for line in output_lines[:20]:
            formatted_lines.append(f"    {line}")

        # Add truncation message if needed
        if len(output_lines) > 20:
            remaining = len(output_lines) - 20
            formatted_lines.append(f"    ... ({remaining} more lines)")

        return formatted_lines

    def _format_edit_diff(self, result) -> List[str]:
        """Format file edit as a pretty condensed diff.

        Args:
            result: Tool execution result with diff_info

        Returns:
            List of formatted diff lines
        """
        diff_info = result.metadata.get('diff_info', {})
        find_text = diff_info.get('find', '')
        replace_text = diff_info.get('replace', '')
        count = diff_info.get('count', 0)
        line_numbers = diff_info.get('lines', [])  # First few line numbers where edit occurred

        formatted_lines = []

        # Show the first line of output (✅ Replaced...)
        first_line = result.output.split('\n')[0]
        formatted_lines.append(f"    {first_line}")

        # Add pretty diff visualization
        formatted_lines.append("")

        # Calculate starting line number for display
        start_line = line_numbers[0] if line_numbers else None

        # Removed lines (red with -) with line numbers
        removed_lines = find_text.split('\n')
        for i, line in enumerate(removed_lines[:3]):  # Show max 3 lines
            if start_line:
                line_num = start_line + i
                formatted_lines.append(f"    \033[31m│- {line_num:4d} {line}\033[0m")
            else:
                formatted_lines.append(f"    \033[31m│- {line}\033[0m")

        if len(removed_lines) > 3:
            formatted_lines.append(f"    \033[31m│  ... ({len(removed_lines) - 3} more lines)\033[0m")

        # Separator
        formatted_lines.append("    \033[90m│\033[0m")

        # Added lines (green with +) with line numbers
        added_lines = replace_text.split('\n')
        for i, line in enumerate(added_lines[:3]):  # Show max 3 lines
            if start_line:
                line_num = start_line + i
                formatted_lines.append(f"    \033[32m│+ {line_num:4d} {line}\033[0m")
            else:
                formatted_lines.append(f"    \033[32m│+ {line}\033[0m")

        if len(added_lines) > 3:
            formatted_lines.append(f"    \033[32m│  ... ({len(added_lines) - 3} more lines)\033[0m")

        formatted_lines.append("")

        # Add backup info if present
        output_lines = result.output.split('\n')
        for line in output_lines[1:]:  # Skip first line (already shown)
            if line.strip():
                formatted_lines.append(f"    {line}")

        return formatted_lines

    def display_generating_progress(self, estimated_tokens: int) -> None:
        """Display generating progress with token estimate.
        
        Args:
            estimated_tokens: Estimated number of tokens being generated
        """
        if estimated_tokens > 0:
            self.renderer.update_thinking(True, f"Receiving... ({estimated_tokens} tokens)")
        else:
            self.renderer.update_thinking(True, "Receiving...")
        logger.debug(f"Displaying receiving progress: {estimated_tokens} tokens")
    
    def clear_thinking_display(self) -> None:
        """Clear thinking/generating display."""
        self.renderer.update_thinking(False)
        logger.debug("Cleared thinking display")

    def show_loading(self, message: str = "Loading...") -> None:
        """Show loading indicator with custom message.

        Args:
            message: Loading message to display (default: "Loading...")
        """
        self.renderer.update_thinking(True, message)
        logger.debug(f"Showing loading: {message}")

    def hide_loading(self) -> None:
        """Hide loading indicator.""" 
        self.renderer.update_thinking(False)
        logger.debug("Hiding loading indicator")

    def start_streaming_response(self) -> None:
        """Start a streaming response session.

        This method initializes streaming mode, disabling atomic batching
        for the duration of the response to allow real-time display.
        """
        self._streaming_active = True
        logger.debug("Started streaming response session")

    def end_streaming_response(self) -> None:
        """End a streaming response session.

        This method disables streaming mode and returns to normal
        atomic batching behavior.
        """
        self._streaming_active = False
        logger.debug("Ended streaming response session")

    def is_streaming_active(self) -> bool:
        """Check if streaming mode is currently active.

        Returns:
            True if streaming is active, False otherwise
        """
        return self._streaming_active

    def display_complete_response(self,
                                 thinking_duration: float,
                                 response: str,
                                 tool_results: List[Any] = None,
                                 original_tools: List[Dict] = None,
                                 show_thinking_threshold: float = 0.1,
                                 skip_response_content: bool = False) -> None:
        """Display complete response with thinking, content, and tools atomically.

        This unified method ensures that thinking duration, assistant response,
        and tool execution results all display together in a single atomic
        operation, preventing commands from appearing after the response.

        Args:
            thinking_duration: Time spent thinking in seconds
            response: Assistant response content
            tool_results: List of tool execution result objects (optional)
            original_tools: List of original tool data for command extraction (optional)
            show_thinking_threshold: Minimum duration to show thinking message
            skip_response_content: Skip displaying response content (for streaming mode)
        """
        message_sequence = []
        pipe_mode = getattr(self.renderer, 'pipe_mode', False)

        # Add thinking duration if meaningful (suppress in pipe mode)
        if thinking_duration > show_thinking_threshold and not pipe_mode:
            thought_message = f"Thought for {thinking_duration:.1f} seconds"
            message_sequence.append(("system", thought_message, {}))

        # Add assistant response if present and not skipped (for streaming mode)
        if response.strip() and not skip_response_content:
            message_sequence.append(("assistant", response, {}))

        # Add tool results if present (suppress in pipe mode)
        if tool_results and not pipe_mode:
            for i, result in enumerate(tool_results):
                # Get original tool data for display
                tool_data = original_tools[i] if original_tools and i < len(original_tools) else {}

                # Extract tool name and arguments for modern rendering
                tool_name, tool_args = self._extract_tool_info(result, tool_data)
                tool_status = "success" if result.success else "error"

                # Get result summary for inline display
                result_summary = self._get_tool_result_summary(result, tool_data)

                # Build tool result content (output only, summary goes inline)
                result_lines = []

                # Add actual output if appropriate
                if self._should_show_output(result):
                    output_lines = self._format_tool_output(result)
                    result_lines.extend(output_lines)

                # Add tool message to sequence with structured kwargs
                message_sequence.append((
                    "tool",
                    "\n".join(result_lines),
                    {
                        "tool_name": tool_name,
                        "tool_args": tool_args,
                        "tool_status": tool_status,
                        "result_summary": result_summary
                    }
                ))

        # Display everything atomically to prevent race conditions
        if message_sequence:
            self.message_coordinator.display_message_sequence(message_sequence)
            logger.debug(f"Displayed complete response with {len(message_sequence)} messages atomically")

    def get_display_stats(self) -> Dict[str, int]:
        """Get display operation statistics.

        Returns:
            Dictionary with display operation counts
        """
        # This could be enhanced with actual counters if needed
        return {
            "messages_displayed": 0,  # Placeholder - could track actual counts
            "tool_results_displayed": 0,
            "thinking_displays": 0,
            "streaming_sessions": 1 if self._streaming_active else 0
        }